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.
192 lines
7.5 KiB
Go
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),
|
|
},
|
|
}
|
|
}
|