Files
mesh-catalog/modules/xorg/cmd/xorg/main.go
T
jschoubben 4fe8958dda
mesh/merge-gate pass: builds xorg → g14, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered
xorg: refuse a window that is not on the screen, and keep an inline screenshot within the bus (review of #154)
A window that is unmapped or on a hidden workspace was cut from the screen anyway, which shows what is
there instead of it. An inline image of up to 4 MiB could not cross the bus, whose largest message is
1 MiB; it is now held to 700 KiB and halved in size until it fits, saying so.
2026-10-09 12:23:51 +02:00

187 lines
9.5 KiB
Go

// xorg's tools (novox/hq ADR 0208, research 026/05): node-display-server's verbs `displays` and
// `layout`, and the module's own tools for one output's mode, the primary output, DPI, input devices,
// the keyboard map, a screenshot and the server's log.
//
// Every tool that touches the screen acts in the operator's running session, which it finds through
// internal/desktop: the node's runtime has none of its own. With no session, the answer says so as
// structured data. A tool that changes the running server changes it until the next login; the
// answer says what makes it last.
package main
import (
"context"
"fmt"
"os"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"xorg/internal/desktop"
)
// The session's holders this module prefers as the session's own process.
var sessionHolders = []string{"i3", "sway", "openbox", "bspwm", "awesome", "xmonad"}
func main() {
if err := stdio.Serve("", tools(desktop.Machine(sessionHolders...))); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
// call bounds one tool call below the runtime's 30 s limit.
func call(run func(ctx context.Context, a desktop.Args) (any, error)) func(map[string]any) (any, error) {
return func(args map[string]any) (any, error) {
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
defer cancel()
return run(ctx, desktop.Args(args))
}
}
func tools(d desktop.Desk) []stdio.Tool {
x := xorg{d: d}
return []stdio.Tool{
{
Name: "node-display-server.displays",
Description: "The X screen and every output: connected or not, primary, where it sits, its rotation, " +
"physical size and DPI, the monitor's identity from its EDID (manufacturer, product, serial, name, " +
"fingerprint), the current and preferred mode, and every mode with its rates. Disconnected outputs " +
"are listed by name only unless all is true. Also the autorandr profile in force, when one matches.",
Input: desktop.Schema(map[string]any{
"all": desktop.Flag("list disconnected outputs in full (default false)"),
"modes": desktop.Flag("list every mode of each output (default true)"),
}),
Run: call(x.displays),
},
{
Name: "node-display-server.layout",
Description: "Monitor layout profiles (autorandr), keyed by the connected monitors' identities, so one " +
"profile needs no machine's name. list: every profile with the monitors it is for, which match " +
"the monitors connected now and which is in force. save: the current arrangement under a name " +
"(replace true to overwrite). apply: load one. The profiles are the operator's data, never the mesh's.",
Input: desktop.Schema(map[string]any{
"action": desktop.Enum("list, save or apply", "list", "save", "apply"),
"name": desktop.Str("the profile to save or apply"),
"replace": desktop.Flag("save over an existing profile of that name (default false)"),
}, "action"),
Run: call(x.layout),
},
{
Name: "xorg_set_mode",
Description: "Change one output now, through xrandr: its mode (WxH, or auto for the preferred one), rate, " +
"rotation, scale, position (x/y, or beside another output), on or off, primary. dry_run answers " +
"the command without running it. Lasts until the next login or hotplug: save a layout profile to keep it.",
Input: desktop.Schema(map[string]any{
"output": desktop.Str("the output, as displays names it (e.g. eDP-1)"),
"mode": desktop.Str("WxH, or auto for the monitor's preferred mode"),
"rate": desktop.Num("refresh rate in Hz"),
"rotate": desktop.Enum("rotation", "normal", "left", "right", "inverted"),
"scale": desktop.Num("scale factor, 0.25 to 4 (1 = none)"),
"x": desktop.Int("left edge on the screen, in pixels"),
"y": desktop.Int("top edge on the screen, in pixels"),
"relation": desktop.Enum("place it beside another output instead of at x/y", "right-of", "left-of", "above", "below", "same-as"),
"of": desktop.Str("the output the relation is to"),
"off": desktop.Flag("switch the output off"),
"primary": desktop.Flag("make it the primary output"),
"dry_run": desktop.Flag("answer the command, run nothing"),
}, "output"),
Run: call(x.setMode),
},
{
Name: "xorg_primary",
Description: "Which output is primary (the one the bar's tray and new windows prefer); with output, make it primary now.",
Input: desktop.Schema(map[string]any{"output": desktop.Str("the output to make primary (optional)")}),
Run: call(x.primary),
},
{
Name: "xorg_dpi",
Description: "The DPI X programs are told (Xft.dpi) beside each monitor's physical DPI. With value, set " +
"Xft.dpi and the screen's DPI now: programs started after it use it. The module's resources file " +
"sets it again at the next login (96 until it is a setting).",
Input: desktop.Schema(map[string]any{"value": desktop.Int("dots per inch, 48 to 480 (optional)")}),
Run: call(x.dpi),
},
{
Name: "xorg_input_devices",
Description: "The input devices the X server knows: id, name, pointer or keyboard, master or slave, and " +
"for each pointer its libinput settings (tap, natural scroll, acceleration, disable while typing, " +
"left handed, enabled).",
Input: desktop.Schema(map[string]any{"name": desktop.Str("only devices whose name contains this (optional)")}),
Run: call(x.inputDevices),
},
{
Name: "xorg_input_set",
Description: "Change libinput settings of a device now: tap, tap_drag, natural_scroll, disable_while_typing, " +
"left_handed, middle_emulation, enabled (true/false), accel_speed (-1 to 1). The device is an id or a " +
"name; a name applies to every device of that name that has the setting. Lasts until the device " +
"reconnects or the next login.",
Input: desktop.Schema(map[string]any{
"device": desktop.Str("the device's id or exact name"),
"tap": desktop.Flag("tap to click"),
"tap_drag": desktop.Flag("tap and drag"),
"natural_scroll": desktop.Flag("natural (reversed) scrolling"),
"disable_while_typing": desktop.Flag("ignore the touchpad while typing"),
"left_handed": desktop.Flag("swap the buttons"),
"middle_emulation": desktop.Flag("both buttons together are the middle one"),
"enabled": desktop.Flag("the device takes input at all"),
"accel_speed": desktop.Num("pointer acceleration, -1 to 1"),
}, "device"),
Run: call(x.inputSet),
},
{
Name: "xorg_keyboard",
Description: "The X keyboard map: rules, model, layout, variant and options. With layout, variant, model " +
"or options, set them now (options replace the current ones; an empty list clears them). Lasts " +
"until the next login.",
Input: desktop.Schema(map[string]any{
"layout": desktop.Str("e.g. us, be, us,be"),
"variant": desktop.Str("e.g. intl"),
"model": desktop.Str("e.g. pc105"),
"options": desktop.List("e.g. [\"caps:escape\", \"compose:ralt\"]"),
}),
Run: call(x.keyboard),
},
{
Name: "xorg_screenshot",
Description: "Take a screenshot to a PNG file: the whole screen, one output, or one window by its X id " +
"(cut from the screen as the compositor shows it, so a see-through window shows what is behind it). " +
"Written under the account's home (default ~/Pictures/Screenshots/screenshot-<time>.png). Answers " +
"the path, size and bytes; with inline, also the PNG itself as base64 (at most 700 KiB, halved in size until it " +
"fits; inline_scale says so). A window that is not viewable (unmapped, or on a workspace not shown) is refused.",
Input: desktop.Schema(map[string]any{
"output": desktop.Str("only this output (optional)"),
"window": desktop.Str("only this window, by its X id, e.g. 0x3a00007 (optional)"),
"path": desktop.Str("where to write it, inside the home (optional)"),
"inline": desktop.Flag("also answer the PNG as base64, at most 700 KiB, for a session that cannot read the file (default false)"),
}),
Run: call(x.screenshot),
},
{
Name: "xorg_windows",
Description: "The windows on the X screen as the server knows them, managed or not (dialogs, tooltips, " +
"windows no window manager lists): id, name, instance and class, size and absolute place, level in " +
"the tree. With class or name (regular expressions), or detail, also each window's depth (argb: a " +
"32-bit visual a compositor blends with what is behind it), visual class, map state, override-redirect, " +
"window types and states, role and own opacity — for at most 40 windows. Without a filter, only " +
"windows with a class, unless all.",
Input: desktop.Schema(map[string]any{
"class": desktop.Str("only windows whose class or instance matches this regular expression"),
"name": desktop.Str("only windows whose name (title) matches this regular expression"),
"detail": desktop.Flag("read each window's details (default false; true when class or name is given)"),
"all": desktop.Flag("without a filter, also windows without a class (default false)"),
}),
Run: call(x.windows),
},
{
Name: "xorg_x_log",
Description: "The X server's log for the running session (or the newest one): its version, when it " +
"started, and its errors — with warnings when asked — newest last.",
Input: desktop.Schema(map[string]any{
"warnings": desktop.Flag("include warnings (default false)"),
"limit": desktop.Int("at most this many lines (default 100, at most 1000)"),
}),
Run: call(x.xlog),
},
}
}