// 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), }, } }