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 }