Files
mesh-catalog/modules/i3/cmd/i3-tools/tree.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

262 lines
7.9 KiB
Go

package main
import (
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
"i3/internal/desktop"
)
// Node is a container of i3's tree, as GET_TREE answers it.
type Node struct {
ID int64 `json:"id"`
Type string `json:"type"`
Name *string `json:"name"`
Layout string `json:"layout"`
Orientation string `json:"orientation"`
Percent *float64 `json:"percent"`
Border string `json:"border"`
BorderWidth int `json:"current_border_width"`
Floating string `json:"floating"`
Focused bool `json:"focused"`
Urgent bool `json:"urgent"`
Marks []string `json:"marks"`
Window *int64 `json:"window"`
WindowProperties map[string]any `json:"window_properties"`
Fullscreen int `json:"fullscreen_mode"`
Scratchpad string `json:"scratchpad_state"`
Geometry map[string]int `json:"geometry"`
Rect map[string]int `json:"rect"`
Nodes []Node `json:"nodes"`
FloatingNodes []Node `json:"floating_nodes"`
}
// Window is one window, as the windows verb answers it.
type Window struct {
ConID int64 `json:"con_id"`
Window string `json:"window"`
Class string `json:"class,omitempty"`
Instance string `json:"instance,omitempty"`
Role string `json:"role,omitempty"`
Title string `json:"title"`
Workspace string `json:"workspace"`
Output string `json:"output"`
Focused bool `json:"focused,omitempty"`
Urgent bool `json:"urgent,omitempty"`
Floating bool `json:"floating,omitempty"`
Fullscreen bool `json:"fullscreen,omitempty"`
Scratchpad bool `json:"scratchpad,omitempty"`
Marks []string `json:"marks,omitempty"`
}
func str(p *string) string {
if p == nil {
return ""
}
return *p
}
func prop(n Node, key string) string {
if v, ok := n.WindowProperties[key].(string); ok {
return v
}
return ""
}
// Windows is every window of the tree with the workspace and output it is on. The scratchpad's
// windows are on the workspace "__i3_scratch" and say so; the bars, in the dock areas, are not windows
// a person arranges and are left out.
func Windows(root Node) []Window {
var out []Window
var walk func(n Node, output, workspace string, dock, floating bool)
walk = func(n Node, output, workspace string, dock, floating bool) {
switch n.Type {
case "output":
output = str(n.Name)
case "workspace":
workspace = str(n.Name)
case "dockarea":
dock = true
case "floating_con":
floating = true
}
if n.Window != nil && !dock {
out = append(out, Window{
ConID: n.ID, Window: "0x" + strconv.FormatInt(*n.Window, 16),
Class: prop(n, "class"), Instance: prop(n, "instance"), Role: prop(n, "window_role"),
Title: str(n.Name), Workspace: workspace, Output: output,
Focused: n.Focused, Urgent: n.Urgent, Floating: floating || strings.HasSuffix(n.Floating, "_on"),
Fullscreen: n.Fullscreen != 0, Scratchpad: workspace == "__i3_scratch", Marks: n.Marks,
})
}
for _, c := range n.Nodes {
walk(c, output, workspace, dock, floating)
}
for _, c := range n.FloatingNodes {
walk(c, output, workspace, dock, true)
}
}
walk(root, "", "", false, false)
return out
}
// Workspace is the subtree of one workspace, by name; or the focused one's when name is empty.
func Workspace(root Node, name string) (Node, bool) {
var found *Node
var walk func(n Node, ws *Node)
walk = func(n Node, ws *Node) {
if found != nil {
return
}
if n.Type == "workspace" {
nn := n
ws = &nn
if name != "" && str(n.Name) == name {
found = ws
return
}
}
if name == "" && n.Focused && ws != nil {
found = ws
return
}
for _, c := range append(append([]Node{}, n.Nodes...), n.FloatingNodes...) {
walk(c, ws)
}
}
walk(root, nil)
if found == nil {
return Node{}, false
}
return *found, true
}
// Layout is a workspace's arrangement in the form i3's append_layout reads (i3's layout saving
// docs): each container with its layout, share and border, each window replaced by a placeholder that
// swallows the next window matching its class, instance and role. One JSON object per top-level
// container, as i3-save-tree writes them.
func Layout(ws Node) ([]map[string]any, int) {
windows := 0
var conv func(n Node) map[string]any
conv = func(n Node) map[string]any {
out := map[string]any{"border": n.Border, "current_border_width": n.BorderWidth, "floating": n.Floating, "layout": n.Layout, "type": "con"}
if n.Percent != nil {
out["percent"] = *n.Percent
}
if n.Name != nil {
out["name"] = *n.Name
}
if len(n.Marks) > 0 {
out["marks"] = n.Marks
}
if n.Window != nil {
windows++
swallow := map[string]string{}
for key, field := range map[string]string{"class": "class", "instance": "instance", "window_role": "window_role"} {
if v := prop(n, key); v != "" {
swallow[field] = "^" + regexp.QuoteMeta(v) + "$"
}
}
out["swallows"] = []map[string]string{swallow}
return out
}
var kids []map[string]any
for _, c := range n.Nodes {
kids = append(kids, conv(c))
}
if kids != nil {
out["nodes"] = kids
}
return out
}
var top []map[string]any
for _, c := range ws.Nodes {
top = append(top, conv(c))
}
for _, f := range ws.FloatingNodes {
fc := conv(f)
fc["type"] = "floating_con"
if g := f.Rect; g != nil {
fc["rect"] = g
}
top = append(top, fc)
}
return top, windows
}
// LayoutFile is a saved layout as written to disk: a comment naming the workspace, then the objects.
func LayoutFile(workspace, saved string, top []map[string]any) string {
var b strings.Builder
fmt.Fprintf(&b, "// mesh i3 layout of workspace %s, saved %s\n", strconv.Quote(workspace), saved)
for _, t := range top {
raw, _ := json.MarshalIndent(t, "", " ")
b.Write(raw)
b.WriteString("\n")
}
return b.String()
}
var savedWorkspace = regexp.MustCompile(`^// mesh i3 layout of workspace ("(?:[^"\\]|\\.)*")`)
// SavedWorkspace is the workspace a saved layout was taken from.
func SavedWorkspace(file string) string {
if m := savedWorkspace.FindStringSubmatch(file); m != nil {
if s, err := strconv.Unquote(m[1]); err == nil {
return s
}
}
return ""
}
// Criteria builds i3's window criteria from a tool's arguments: con_id, window (X id), class,
// instance, title, mark, each quoted. Empty when none is given.
func Criteria(a desktop.Args) (string, error) {
var parts []string
if a.Has("con_id") {
f, _, err := a.Number("con_id")
if err != nil || f <= 0 {
return "", fmt.Errorf("con_id is a container's id, as windows answers it")
}
parts = append(parts, "con_id="+strconv.FormatInt(int64(f), 10))
}
if w := a.Opt("window", ""); w != "" {
n, err := strconv.ParseInt(strings.TrimPrefix(strings.ToLower(w), "0x"), 16, 64)
if err != nil || !strings.HasPrefix(strings.ToLower(w), "0x") {
return "", fmt.Errorf("window is an X id, e.g. 0x3a00007")
}
parts = append(parts, "id="+strconv.FormatInt(n, 10))
}
for _, k := range []string{"class", "instance", "title", "con_mark"} {
arg := k
if k == "con_mark" {
arg = "mark"
}
if v := a.Opt(arg, ""); v != "" {
parts = append(parts, k+"="+Quote(v))
}
}
if len(parts) == 0 {
return "", nil
}
return "[" + strings.Join(parts, " ") + "]", nil
}
// criteriaProps are the arguments every window-targeting tool takes.
func criteriaProps(extra map[string]any) map[string]any {
p := map[string]any{
"con_id": desktop.Int("the container's id, as windows answers it"),
"window": desktop.Str("the X window id, e.g. 0x3a00007"),
"class": desktop.Str("windows whose class matches this (a regular expression)"),
"instance": desktop.Str("windows whose instance matches this"),
"title": desktop.Str("windows whose title matches this"),
"mark": desktop.Str("the window holding this mark"),
}
for k, v := range extra {
p[k] = v
}
return p
}