xorg: the display server holds node-display-server and writes the session's start (hq ADR 0208)
The X server, its start and its tools as one module. It provides x11-display with the machine's reach, gated by the host's seat capability. It writes a block at the start of ~/.xinitrc: the account's environment, an explicit import into the user manager, the mesh's X resources merged without cpp, autorandr, the xinitrc slots, ~/.xinitrc.local, and the session's exec from the last slot. Its Go tools serve the seat's displays and layout (autorandr profiles keyed by EDID), and set-mode, primary, dpi, input devices and settings, keyboard, a screenshot (xwd decoded in Go) and the server's log. internal/desktop is how every desktop tool finds the operator's session from the runtime, which has none: from the session's own processes, reading only its words, confirmed with logind.
This commit is contained in:
@@ -0,0 +1,602 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/draw"
|
||||
"image/png"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"xorg/internal/desktop"
|
||||
)
|
||||
|
||||
type xorg struct{ d desktop.Desk }
|
||||
|
||||
// restored is what every answer that changed the running server says about lasting.
|
||||
const untilLogin = "until the next login: the session's start applies the declared defaults and the matching layout profile again"
|
||||
|
||||
var outputName = regexp.MustCompile(`^[A-Za-z0-9_.-]+$`)
|
||||
|
||||
func (x xorg) query(ctx context.Context) (Screen, []Output, *desktop.Session, error) {
|
||||
res, s, err := x.d.InSession(ctx, "xrandr", "--prop")
|
||||
if err != nil {
|
||||
return Screen{}, nil, nil, err
|
||||
}
|
||||
if !res.OK() {
|
||||
return Screen{}, nil, s, res.Err()
|
||||
}
|
||||
screen, outs := ParseXrandr(res.Stdout)
|
||||
return screen, outs, s, nil
|
||||
}
|
||||
|
||||
func (x xorg) displays(ctx context.Context, a desktop.Args) (any, error) {
|
||||
all, _, err := a.Bool("all")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
modes, given, err := a.Bool("modes")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !given {
|
||||
modes = true
|
||||
}
|
||||
screen, outs, s, err := x.query(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var shown []Output
|
||||
var off []string
|
||||
for _, o := range outs {
|
||||
if !o.Connected && !all {
|
||||
off = append(off, o.Name)
|
||||
continue
|
||||
}
|
||||
if !modes {
|
||||
o.Modes = nil
|
||||
}
|
||||
shown = append(shown, o)
|
||||
}
|
||||
answer := map[string]any{"session": s.Display, "screen": screen, "outputs": shown, "disconnected": off}
|
||||
if r := x.d.Run(ctx, s.Env(x.d.Base), nil, "autorandr", "--current"); r.OK() {
|
||||
answer["profile"] = firstName(r.Stdout)
|
||||
}
|
||||
return answer, nil
|
||||
}
|
||||
|
||||
func firstName(text string) string {
|
||||
for n := range names(text) {
|
||||
return n
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x xorg) layout(ctx context.Context, a desktop.Args) (any, error) {
|
||||
action, err := a.OneOf("action", "", "list", "save", "apply")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dirs := ProfileDirs(desktop.Home())
|
||||
if action == "list" {
|
||||
profiles := ReadProfiles(dirs)
|
||||
answer := map[string]any{"profiles": profiles, "where": dirs[0]}
|
||||
s, err := x.d.Find()
|
||||
if err != nil {
|
||||
answer["session"] = nil
|
||||
answer["note"] = "no graphical session: which profile matches the monitors connected is not known"
|
||||
return answer, nil
|
||||
}
|
||||
env := s.Env(x.d.Base)
|
||||
det := x.d.Run(ctx, env, nil, "autorandr", "--detected")
|
||||
if det.Code == 127 {
|
||||
return nil, fmt.Errorf("autorandr is not installed here; the xorg module's package resource installs it")
|
||||
}
|
||||
detected, current := names(det.Stdout), names(x.d.Run(ctx, env, nil, "autorandr", "--current").Stdout)
|
||||
for i := range profiles {
|
||||
profiles[i].Detected, profiles[i].Current = detected[profiles[i].Name], current[profiles[i].Name]
|
||||
}
|
||||
answer["profiles"] = profiles
|
||||
return answer, nil
|
||||
}
|
||||
name, err := a.Text("name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !profileName.MatchString(name) {
|
||||
return nil, fmt.Errorf("a profile name is letters, digits, dot, dash and underscore, up to 64")
|
||||
}
|
||||
args := []string{"--load", name}
|
||||
if action == "save" {
|
||||
replace, _, err := a.Bool("replace")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args = []string{"--save", name}
|
||||
if replace {
|
||||
args = append(args, "--force")
|
||||
}
|
||||
}
|
||||
res, s, err := x.d.InSession(ctx, "autorandr", args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.Code == 127 {
|
||||
return nil, fmt.Errorf("autorandr is not installed here; the xorg module's package resource installs it")
|
||||
}
|
||||
if !res.OK() {
|
||||
return nil, res.Err()
|
||||
}
|
||||
answer := map[string]any{"action": action, "name": name, "session": s.Display, "said": strings.TrimSpace(res.Stdout + res.Stderr)}
|
||||
if action == "save" {
|
||||
answer["kept"] = "in " + dirs[0] + ": the operator's data; the session's start applies it whenever these monitors are connected"
|
||||
}
|
||||
return answer, nil
|
||||
}
|
||||
|
||||
// ModeArgs is the xrandr command line set-mode asks for, refused before anything runs when it is not
|
||||
// one xrandr would take.
|
||||
func ModeArgs(a desktop.Args) ([]string, error) {
|
||||
output, err := a.Text("output")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !outputName.MatchString(output) {
|
||||
return nil, fmt.Errorf("%q is not an output name", output)
|
||||
}
|
||||
args := []string{"--output", output}
|
||||
off, _, err := a.Bool("off")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if off {
|
||||
return append(args, "--off"), nil
|
||||
}
|
||||
switch mode := a.Opt("mode", ""); {
|
||||
case mode == "auto":
|
||||
args = append(args, "--auto")
|
||||
case mode != "":
|
||||
if !regexp.MustCompile(`^\d{2,5}x\d{2,5}i?$`).MatchString(mode) {
|
||||
return nil, fmt.Errorf("mode is WxH, e.g. 2560x1440, or auto")
|
||||
}
|
||||
args = append(args, "--mode", mode)
|
||||
}
|
||||
if rate, given, err := a.Number("rate"); err != nil {
|
||||
return nil, err
|
||||
} else if given {
|
||||
if rate < 1 || rate > 1000 {
|
||||
return nil, fmt.Errorf("rate is in Hz, 1 to 1000")
|
||||
}
|
||||
args = append(args, "--rate", strconv.FormatFloat(rate, 'f', 2, 64))
|
||||
}
|
||||
if a.Has("rotate") {
|
||||
r, err := a.OneOf("rotate", "", "normal", "left", "right", "inverted")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args = append(args, "--rotate", r)
|
||||
}
|
||||
if scale, given, err := a.Number("scale"); err != nil {
|
||||
return nil, err
|
||||
} else if given {
|
||||
if scale < 0.25 || scale > 4 {
|
||||
return nil, fmt.Errorf("scale is from 0.25 to 4")
|
||||
}
|
||||
f := strconv.FormatFloat(scale, 'f', -1, 64)
|
||||
args = append(args, "--scale", f+"x"+f)
|
||||
}
|
||||
if a.Has("relation") {
|
||||
rel, err := a.OneOf("relation", "", "right-of", "left-of", "above", "below", "same-as")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
of, err := a.Text("of")
|
||||
if err != nil || !outputName.MatchString(of) {
|
||||
return nil, fmt.Errorf("relation needs of: the output it is beside")
|
||||
}
|
||||
args = append(args, "--"+rel, of)
|
||||
} else if a.Has("x") || a.Has("y") {
|
||||
px, err := a.Whole("x", 0, -32768, 32768)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
py, err := a.Whole("y", 0, -32768, 32768)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args = append(args, "--pos", fmt.Sprintf("%dx%d", px, py))
|
||||
}
|
||||
if p, _, err := a.Bool("primary"); err != nil {
|
||||
return nil, err
|
||||
} else if p {
|
||||
args = append(args, "--primary")
|
||||
}
|
||||
if len(args) == 2 {
|
||||
return nil, fmt.Errorf("nothing to change: give a mode, rate, rotate, scale, position, off or primary")
|
||||
}
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func (x xorg) setMode(ctx context.Context, a desktop.Args) (any, error) {
|
||||
args, err := ModeArgs(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dry, _, _ := a.Bool("dry_run"); dry {
|
||||
return map[string]any{"dry_run": true, "command": append([]string{"xrandr"}, args...)}, nil
|
||||
}
|
||||
res, s, err := x.d.InSession(ctx, "xrandr", args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !res.OK() {
|
||||
return nil, res.Err()
|
||||
}
|
||||
_, outs, _, _ := x.query(ctx)
|
||||
var now *Output
|
||||
for i := range outs {
|
||||
if outs[i].Name == args[1] {
|
||||
o := outs[i]
|
||||
o.Modes = nil
|
||||
now = &o
|
||||
}
|
||||
}
|
||||
return map[string]any{"session": s.Display, "command": res.Command, "output": now, "lasts": untilLogin}, nil
|
||||
}
|
||||
|
||||
func (x xorg) primary(ctx context.Context, a desktop.Args) (any, error) {
|
||||
if name := a.Opt("output", ""); name != "" {
|
||||
if !outputName.MatchString(name) {
|
||||
return nil, fmt.Errorf("%q is not an output name", name)
|
||||
}
|
||||
res, _, err := x.d.InSession(ctx, "xrandr", "--output", name, "--primary")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !res.OK() {
|
||||
return nil, res.Err()
|
||||
}
|
||||
}
|
||||
_, outs, s, err := x.query(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
answer := map[string]any{"session": s.Display, "primary": nil}
|
||||
for _, o := range outs {
|
||||
if o.Primary {
|
||||
answer["primary"] = o.Name
|
||||
}
|
||||
}
|
||||
if a.Has("output") {
|
||||
answer["lasts"] = untilLogin
|
||||
}
|
||||
return answer, nil
|
||||
}
|
||||
|
||||
// ResourceValue is one resource's value from `xrdb -query` output.
|
||||
func ResourceValue(query, name string) string {
|
||||
for _, l := range strings.Split(query, "\n") {
|
||||
if k, v, ok := strings.Cut(l, ":"); ok && strings.TrimSpace(k) == name {
|
||||
return strings.TrimSpace(v)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x xorg) dpi(ctx context.Context, a desktop.Args) (any, error) {
|
||||
value, err := a.Whole("value", 0, 48, 480)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s, err := x.d.Find()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
env := s.Env(x.d.Base)
|
||||
if value != 0 {
|
||||
v := strconv.Itoa(value)
|
||||
if r := x.d.Run(ctx, env, []byte("Xft.dpi: "+v+"\n"), "xrdb", "-nocpp", "-merge", "-"); !r.OK() {
|
||||
return nil, r.Err()
|
||||
}
|
||||
if r := x.d.Run(ctx, env, nil, "xrandr", "--dpi", v); !r.OK() {
|
||||
return nil, r.Err()
|
||||
}
|
||||
}
|
||||
q := x.d.Run(ctx, env, nil, "xrdb", "-query")
|
||||
if !q.OK() {
|
||||
return nil, q.Err()
|
||||
}
|
||||
_, outs, _, err := x.query(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
physical := map[string]float64{}
|
||||
for _, o := range outs {
|
||||
if o.DPI > 0 {
|
||||
physical[o.Name] = o.DPI
|
||||
}
|
||||
}
|
||||
answer := map[string]any{"session": s.Display, "xft_dpi": ResourceValue(q.Stdout, "Xft.dpi"), "physical": physical}
|
||||
if value != 0 {
|
||||
answer["set"] = value
|
||||
answer["lasts"] = "for programs started from now on, " + untilLogin
|
||||
}
|
||||
return answer, nil
|
||||
}
|
||||
|
||||
func (x xorg) devices(ctx context.Context, env []string) ([]Device, error) {
|
||||
r := x.d.Run(ctx, env, nil, "xinput", "list")
|
||||
if !r.OK() {
|
||||
return nil, r.Err()
|
||||
}
|
||||
return ParseDevices(r.Stdout), nil
|
||||
}
|
||||
|
||||
func (x xorg) inputDevices(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)
|
||||
all, err := x.devices(ctx, env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filter := strings.ToLower(a.Opt("name", ""))
|
||||
var out []Device
|
||||
for _, dev := range all {
|
||||
if filter != "" && !strings.Contains(strings.ToLower(dev.Name), filter) {
|
||||
continue
|
||||
}
|
||||
if dev.Role == "slave" && dev.Kind == "pointer" && !strings.Contains(dev.Name, "XTEST") {
|
||||
if r := x.d.Run(ctx, env, nil, "xinput", "list-props", strconv.Itoa(dev.ID)); r.OK() {
|
||||
dev.Settings = ParseProps(r.Stdout)
|
||||
}
|
||||
}
|
||||
out = append(out, dev)
|
||||
}
|
||||
return map[string]any{"session": s.Display, "devices": out}, nil
|
||||
}
|
||||
|
||||
func (x xorg) inputSet(ctx context.Context, a desktop.Args) (any, error) {
|
||||
which, err := a.Text("device")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
type change struct {
|
||||
knob Knob
|
||||
value string
|
||||
}
|
||||
var changes []change
|
||||
for _, k := range Knobs {
|
||||
if !a.Has(k.Arg) {
|
||||
continue
|
||||
}
|
||||
switch k.Kind {
|
||||
case "bool":
|
||||
b, _, err := a.Bool(k.Arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
changes = append(changes, change{k, map[bool]string{true: "1", false: "0"}[b]})
|
||||
case "float":
|
||||
f, _, err := a.Number(k.Arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if f < -1 || f > 1 {
|
||||
return nil, fmt.Errorf("%s is from -1 to 1", k.Arg)
|
||||
}
|
||||
changes = append(changes, change{k, strconv.FormatFloat(f, 'f', 3, 64)})
|
||||
}
|
||||
}
|
||||
if len(changes) == 0 {
|
||||
return nil, fmt.Errorf("nothing to change: name at least one setting")
|
||||
}
|
||||
s, err := x.d.Find()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
env := s.Env(x.d.Base)
|
||||
all, err := x.devices(ctx, env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var ids []Device
|
||||
for _, dev := range all {
|
||||
if dev.Role == "slave" && (strconv.Itoa(dev.ID) == which || dev.Name == which) {
|
||||
ids = append(ids, dev)
|
||||
}
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return nil, fmt.Errorf("no input device is %q: xorg_input_devices lists them", which)
|
||||
}
|
||||
type outcome struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Set []string `json:"set"`
|
||||
Skipped []string `json:"skipped,omitempty"`
|
||||
Now map[string]any `json:"now"`
|
||||
}
|
||||
var done []outcome
|
||||
for _, dev := range ids {
|
||||
props := x.d.Run(ctx, env, nil, "xinput", "list-props", strconv.Itoa(dev.ID))
|
||||
have := ParseProps(props.Stdout)
|
||||
o := outcome{ID: dev.ID, Name: dev.Name, Set: []string{}}
|
||||
for _, c := range changes {
|
||||
if _, ok := have[c.knob.Arg]; !ok {
|
||||
o.Skipped = append(o.Skipped, c.knob.Arg)
|
||||
continue
|
||||
}
|
||||
r := x.d.Run(ctx, env, nil, "xinput", "set-prop", strconv.Itoa(dev.ID), c.knob.Property, c.value)
|
||||
if !r.OK() {
|
||||
return nil, r.Err()
|
||||
}
|
||||
o.Set = append(o.Set, c.knob.Arg)
|
||||
}
|
||||
after := x.d.Run(ctx, env, nil, "xinput", "list-props", strconv.Itoa(dev.ID))
|
||||
o.Now = ParseProps(after.Stdout)
|
||||
done = append(done, o)
|
||||
}
|
||||
return map[string]any{"session": s.Display, "devices": done, "lasts": "until the device reconnects or the next login"}, nil
|
||||
}
|
||||
|
||||
func (x xorg) keyboard(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)
|
||||
var args []string
|
||||
for _, k := range []string{"layout", "variant", "model"} {
|
||||
if v := a.Opt(k, ""); v != "" {
|
||||
if !xkbName.MatchString(v) {
|
||||
return nil, fmt.Errorf("%s %q is not an XKB name", k, v)
|
||||
}
|
||||
args = append(args, "-"+k, v)
|
||||
}
|
||||
}
|
||||
if a.Has("options") {
|
||||
opts, err := a.Strings("options")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args = append(args, "-option", "")
|
||||
for _, o := range opts {
|
||||
if !xkbName.MatchString(o) {
|
||||
return nil, fmt.Errorf("option %q is not an XKB name", o)
|
||||
}
|
||||
args = append(args, "-option", o)
|
||||
}
|
||||
}
|
||||
if len(args) > 0 {
|
||||
if r := x.d.Run(ctx, env, nil, "setxkbmap", args...); !r.OK() {
|
||||
return nil, r.Err()
|
||||
}
|
||||
}
|
||||
q := x.d.Run(ctx, env, nil, "setxkbmap", "-query")
|
||||
if !q.OK() {
|
||||
return nil, q.Err()
|
||||
}
|
||||
answer := map[string]any{"session": s.Display, "keyboard": ParseKeyboard(q.Stdout)}
|
||||
if len(args) > 0 {
|
||||
answer["lasts"] = untilLogin
|
||||
}
|
||||
return answer, nil
|
||||
}
|
||||
|
||||
var windowID = regexp.MustCompile(`^(0x[0-9a-fA-F]{1,8}|[0-9]{1,10})$`)
|
||||
|
||||
func (x xorg) screenshot(ctx context.Context, a desktop.Args) (any, error) {
|
||||
stamp := time.Now().Format("20060102-150405")
|
||||
path, err := desktop.InHome(a.Opt("path", "~/Pictures/Screenshots/screenshot-"+stamp+".png"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !strings.HasSuffix(strings.ToLower(path), ".png") {
|
||||
return nil, fmt.Errorf("the screenshot is a PNG: the path ends in .png")
|
||||
}
|
||||
s, err := x.d.Find()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
env := s.Env(x.d.Base)
|
||||
dump, err := os.CreateTemp("", "xorg-screenshot-*.xwd")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dump.Close()
|
||||
defer os.Remove(dump.Name())
|
||||
args := []string{"-silent", "-out", dump.Name()}
|
||||
if w := a.Opt("window", ""); w != "" {
|
||||
if !windowID.MatchString(w) {
|
||||
return nil, fmt.Errorf("window is an X window id, e.g. 0x3a00007")
|
||||
}
|
||||
args = append(args, "-id", w)
|
||||
} else {
|
||||
args = append(args, "-root")
|
||||
}
|
||||
if r := x.d.Run(ctx, env, nil, "xwd", args...); !r.OK() {
|
||||
if r.Code == 127 {
|
||||
return nil, fmt.Errorf("xwd is not installed here; the xorg module's package resource (xorg-xwd) installs it")
|
||||
}
|
||||
return nil, r.Err()
|
||||
}
|
||||
raw, err := os.ReadFile(dump.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
img, err := DecodeXWD(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the dump xwd wrote: %w", err)
|
||||
}
|
||||
var shot image.Image = img
|
||||
if name := a.Opt("output", ""); name != "" {
|
||||
_, outs, _, err := x.query(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var g *Geometry
|
||||
for _, o := range outs {
|
||||
if o.Name == name {
|
||||
g = o.Geometry
|
||||
}
|
||||
}
|
||||
if g == nil {
|
||||
return nil, fmt.Errorf("output %q is not on: displays lists the outputs in use", name)
|
||||
}
|
||||
r := image.Rect(g.X, g.Y, g.X+g.Width, g.Y+g.Height).Intersect(img.Bounds())
|
||||
crop := image.NewRGBA(image.Rect(0, 0, r.Dx(), r.Dy()))
|
||||
draw.Draw(crop, crop.Bounds(), img, r.Min, draw.Src)
|
||||
shot = crop
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, shot); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.WriteFile(path, buf.Bytes(), 0o644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b := shot.Bounds()
|
||||
return map[string]any{"session": s.Display, "path": path, "width": b.Dx(), "height": b.Dy(), "bytes": buf.Len()}, nil
|
||||
}
|
||||
|
||||
func (x xorg) xlog(ctx context.Context, a desktop.Args) (any, error) {
|
||||
warnings, _, err := a.Bool("warnings")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limit, err := a.Whole("limit", 100, 1, 1000)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
display := -1
|
||||
if s, err := x.d.Find(); err == nil {
|
||||
if n, ok := desktop.DisplayNumber(s.Display); ok {
|
||||
display = n
|
||||
}
|
||||
}
|
||||
files := XLogFiles(display, desktop.Home())
|
||||
if display >= 0 {
|
||||
files = append(files, XLogFiles(-1, desktop.Home())...)
|
||||
}
|
||||
for _, f := range files {
|
||||
raw, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return ParseXLog(f, string(raw), warnings, limit), nil
|
||||
}
|
||||
sort.Strings(files)
|
||||
return nil, fmt.Errorf("no X server log is readable here; looked at %s", strings.Join(files, ", "))
|
||||
}
|
||||
Reference in New Issue
Block a user