Files
mesh-catalog/modules/i3/cmd/i3-tools/main.go
T
jochen 34829e39fe i3: the window manager holds node-display-session; its config improved and a checked reload watcher (hq ADR 0208)
It requires x11-display and contributes exec i3 to xinitrc's last slot, and
XDG_CURRENT_DESKTOP/XDG_SESSION_DESKTOP to the environment. It owns
~/.config/i3/config, ending with the config.d include where other modules drop
their files, and /etc/lemurs/wms/i3, which runs the session's start.

Over today's identical config it drops the dead lxpolkit, the D-Bus-activated
portal, the xrdb merge xorg now does, and the execs that XDG autostart already
started. It sets JetBrainsMono Nerd Font, runs i3-sensible-terminal, and declares
dex, which the desktop lacked.

The tools speak i3's IPC: the seat's reload, workspaces and windows, and focus,
move, layout save/restore, exec, kill, bindings, config check, marks and the
scratchpad. A watcher in the bundle (ADR 0198) replaces the predecessor's inotify
script and user unit. It reloads only a configuration i3 -C accepts, and at its
start whatever i3 has not loaded.
2026-10-04 13:21:47 +02:00

192 lines
7.5 KiB
Go

// i3's tools (novox/hq ADR 0208, research 026/05): node-display-session's verbs `reload`, `workspaces`
// and `windows`, the module's own tools for focus, moving, layouts, exec, kill, bindings, the
// configuration check, marks and the scratchpad — and, running for as long as the bundle does, the
// watcher that reloads i3 when its configuration changes (ADR 0198).
//
// The tools speak i3's IPC on its socket under the account's runtime directory; they need no display.
// With no i3 running they answer that there is no session, as structured data.
package main
import (
"context"
"fmt"
"os"
"path/filepath"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"i3/internal/desktop"
)
func main() {
t := machine()
w := &Watcher{
Dir: t.configDir, Every: 2 * time.Second,
Check: func() ([]ConfigError, error) {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
return t.check(ctx)
},
Reload: func() error {
_, err := t.command("reload")
return err
},
Loaded: t.loaded,
}
t.watcher = w
go w.Run(make(chan struct{}))
if err := stdio.Serve("", tools(t)); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
// machine is the i3 of this machine's operator account.
func machine() *i3 {
home := desktop.Home()
cfg := os.Getenv("XDG_CONFIG_HOME")
if cfg == "" {
cfg = filepath.Join(home, ".config")
}
state := os.Getenv("XDG_STATE_HOME")
if state == "" {
state = filepath.Join(home, ".local", "state")
}
t := &i3{d: desktop.Machine("i3"), configDir: filepath.Join(cfg, "i3"), layouts: filepath.Join(state, "mesh", "i3", "layouts")}
t.socket = func() (string, error) {
runtime := filepath.Join("/run/user", fmt.Sprint(os.Getuid()))
i3sock := ""
if s, err := t.d.Find(); err == nil {
runtime, i3sock = s.RuntimeDir, s.Word("I3SOCK")
}
return FindSocket(runtime, i3sock)
}
return t
}
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(t *i3) []stdio.Tool {
return []stdio.Tool{
{
Name: "node-display-session.reload",
Description: "Reload i3's configuration in place, keeping every window: checked first with i3 -C, and " +
"refused with the errors when it would not load cleanly (force reloads anyway). Answers i3's result " +
"and what the reload watcher has done.",
Input: desktop.Schema(map[string]any{"force": desktop.Flag("reload even when the check finds errors")}),
Run: call(t.reload),
},
{
Name: "node-display-session.workspaces",
Description: "The session's workspaces: number, name, output, and whether each is visible, focused or urgent.",
Input: desktop.Schema(map[string]any{}),
Run: call(t.workspaces),
},
{
Name: "node-display-session.windows",
Description: "The session's windows: container id, X id, class, instance, role, title, workspace and output, " +
"and whether each is focused, urgent, floating, fullscreen or in the scratchpad, with its marks. " +
"Narrowed to one workspace when named.",
Input: desktop.Schema(map[string]any{"workspace": desktop.Str("one workspace, by name (optional)")}),
Run: call(t.windows),
},
{
Name: "i3_focus",
Description: "Focus a window (by con_id, window, class, instance, title or mark) or a workspace (by name; " +
"created if absent, as i3 does).",
Input: desktop.Schema(criteriaProps(map[string]any{"workspace": desktop.Str("the workspace to show")})),
Run: call(t.focus),
},
{
Name: "i3_move",
Description: "Move windows (the focused one, or those the criteria match) to a workspace or an output; or, " +
"with workspace_to_output, move a whole workspace to an output.",
Input: desktop.Schema(criteriaProps(map[string]any{
"to_workspace": desktop.Str("the workspace to move to"),
"to_output": desktop.Str("the output to move to (a name, or left/right/up/down)"),
"workspace_to_output": desktop.Str("move this workspace (by name) to to_output instead of a window"),
})),
Run: call(t.move),
},
{
Name: "i3_layout_save",
Description: "Save a workspace's arrangement (the focused one when none is named) as a named layout: its " +
"containers, splits and shares, each window as a placeholder for the next window of its class, " +
"instance and role. Kept in the account's state directory (~/.local/state/mesh/i3/layouts).",
Input: desktop.Schema(map[string]any{
"name": desktop.Str("the layout's name"),
"workspace": desktop.Str("the workspace to save (optional; the focused one)"),
}, "name"),
Run: call(t.layoutSave),
},
{
Name: "i3_layout_restore",
Description: "Lay a saved layout onto a workspace (the one it was saved from, unless named): its placeholders " +
"wait there and swallow matching windows as they open. With layout list, answers the saved layouts.",
Input: desktop.Schema(map[string]any{
"name": desktop.Str("the layout's name, or list"),
"workspace": desktop.Str("the workspace to lay it on (optional)"),
}, "name"),
Run: call(t.layoutRestore),
},
{
Name: "i3_exec",
Description: "Start a program in the session, through i3 (so it is the session's child, with the session's " +
"environment, and outlives the call). Optionally on a given workspace.",
Input: desktop.Schema(map[string]any{
"command": desktop.Str("the command line, as a shell reads it"),
"workspace": desktop.Str("switch to this workspace first (optional)"),
}, "command"),
Run: call(t.exec),
},
{
Name: "i3_kill",
Description: "Close the windows the criteria match (politely, as the window's close button), or the focused " +
"one when focused is true. Refused with neither, so a call without arguments closes nothing.",
Input: desktop.Schema(criteriaProps(map[string]any{
"focused": desktop.Flag("close the focused window"),
"force": desktop.Flag("kill the client instead of asking the window to close"),
})),
Run: call(t.kill),
},
{
Name: "i3_bindings",
Description: "Every key binding in force and what it runs, by mode, from the configuration i3 loaded " +
"(main file and drop-ins, variables replaced), with the file each comes from. Narrowed by a text " +
"found in the keys or the command.",
Input: desktop.Schema(map[string]any{"match": desktop.Str("only bindings whose keys or command contain this (optional)")}),
Run: call(t.bindings),
},
{
Name: "i3_config_check",
Description: "Check the configuration on disk (main file and every drop-in) with i3 -C, as the next reload " +
"would load it: valid or the errors with file and line; the files i3 loaded last; and what the " +
"reload watcher has done. Needs no running session.",
Input: desktop.Schema(map[string]any{}),
Run: call(t.configCheck),
},
{
Name: "i3_marks",
Description: "Every mark set on a window, with the window it is on.",
Input: desktop.Schema(map[string]any{}),
Run: call(t.marks),
},
{
Name: "i3_scratchpad",
Description: "The scratchpad: list its windows; show (toggle) one — the criteria pick it, else i3 cycles; or " +
"move a window (the criteria's, else the focused one) into it.",
Input: desktop.Schema(criteriaProps(map[string]any{
"action": desktop.Enum("list (default), show or move", "list", "show", "move"),
})),
Run: call(t.scratchpad),
},
}
}