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.
262 lines
7.9 KiB
Go
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
|
|
}
|