package main import ( "context" "fmt" "regexp" "strconv" "strings" "xorg/internal/desktop" ) // The windows on the X screen, as the server knows them (xwininfo): what a window manager's own list // (node-display-session.windows) leaves out — a window it does not manage, a dialog, a tooltip, a // window that is mapped but drawn by nobody — and what a compositor goes by: the window's depth (32 // bits is a visual with an alpha channel, which a compositor blends with what is behind it), its type, // whether it is mapped, and its own opacity. // // The tree comes from one `xwininfo -root -tree`. A window's details take one `xwininfo -all` and one // `xprop` each, so they are read only for the windows asked about, and at most mostDetailed of them. const mostDetailed = 40 // Window is one window of the tree, and its details when they were read. type Window struct { ID string `json:"window"` Name string `json:"name,omitempty"` Instance string `json:"instance,omitempty"` Class string `json:"class,omitempty"` Width int `json:"width"` Height int `json:"height"` X int `json:"x"` Y int `json:"y"` // Level is the window's depth in the tree: 1 is a child of the root (a frame or an unmanaged window). Level int `json:"level"` // The details (detail true, or a class asked for). Depth int `json:"depth,omitempty"` ARGB *bool `json:"argb,omitempty"` Visual string `json:"visual_class,omitempty"` Mapped string `json:"map_state,omitempty"` OverrideRedir *bool `json:"override_redirect,omitempty"` Types []string `json:"types,omitempty"` States []string `json:"states,omitempty"` Role string `json:"role,omitempty"` Opacity *float64 `json:"opacity,omitempty"` DetailProblems string `json:"detail_problem,omitempty"` } // treeLine is one line of `xwininfo -root -tree`: the id, the name (or "(has no name)"), the class // pair when the window has one, the size and position relative to the parent, and the absolute one. var treeLine = regexp.MustCompile(`^(\s*)(0x[0-9a-fA-F]+) (?:\(has no name\)|"(.*)"): \((?:"([^"]*)" "([^"]*)")?\)\s+(\d+)x(\d+)[+-]-?\d+[+-]-?\d+\s+\+?(-?\d+)\+?(-?\d+)\s*$`) // ParseTree reads the windows of `xwininfo -root -tree`, in the server's stacking order per parent. func ParseTree(text string) []Window { var out []Window for _, line := range strings.Split(text, "\n") { m := treeLine.FindStringSubmatch(line) if m == nil { continue } w := Window{ID: m[2], Name: m[3], Instance: m[4], Class: m[5]} w.Width, _ = strconv.Atoi(m[6]) w.Height, _ = strconv.Atoi(m[7]) w.X, _ = strconv.Atoi(m[8]) w.Y, _ = strconv.Atoi(m[9]) // xwininfo indents a child of the root by 5 spaces and each further level by 3. w.Level = 1 + (len(m[1])-5)/3 out = append(out, w) } return out } // ParseDetails adds what `xwininfo -id -all` says about one window. func ParseDetails(w *Window, text string) { section := "" for _, raw := range strings.Split(text, "\n") { line := strings.TrimSpace(raw) indent := len(raw) - len(strings.TrimLeft(raw, " ")) switch { case strings.HasPrefix(line, "Depth:"): w.Depth, _ = strconv.Atoi(strings.TrimSpace(strings.TrimPrefix(line, "Depth:"))) argb := w.Depth == 32 w.ARGB = &argb section = "" case strings.HasPrefix(line, "Visual Class:"): w.Visual = strings.TrimSpace(strings.TrimPrefix(line, "Visual Class:")) case strings.HasPrefix(line, "Map State:"): w.Mapped = strings.TrimSpace(strings.TrimPrefix(line, "Map State:")) case strings.HasPrefix(line, "Override Redirect State:"): v := strings.TrimSpace(strings.TrimPrefix(line, "Override Redirect State:")) == "yes" w.OverrideRedir = &v case line == "Window type:": section = "type" case line == "Window state:": section = "state" case line == "": section = "" case section != "" && indent >= 10: if section == "type" { w.Types = append(w.Types, line) } else { w.States = append(w.States, line) } default: if indent < 10 { section = "" } } } } // opacityLine and roleLine are the two properties read with xprop. var ( opacityLine = regexp.MustCompile(`^_NET_WM_WINDOW_OPACITY\(CARDINAL\) = (\d+)`) roleLine = regexp.MustCompile(`^WM_WINDOW_ROLE\(STRING\) = "(.*)"`) ) // ParseWindowProps adds the window's role and its own opacity (0 to 1) from xprop. func ParseWindowProps(w *Window, text string) { for _, line := range strings.Split(text, "\n") { line = strings.TrimSpace(line) if m := opacityLine.FindStringSubmatch(line); m != nil { if n, err := strconv.ParseUint(m[1], 10, 32); err == nil { v := float64(n) / float64(0xffffffff) v = float64(int(v*1000+0.5)) / 1000 w.Opacity = &v } } if m := roleLine.FindStringSubmatch(line); m != nil { w.Role = m[1] } } } func (x xorg) windows(ctx context.Context, a desktop.Args) (any, error) { classExpr := a.Opt("class", "") nameExpr := a.Opt("name", "") detail, _, err := a.Bool("detail") if err != nil { return nil, err } all, _, err := a.Bool("all") if err != nil { return nil, err } var classRe, nameRe *regexp.Regexp if classExpr != "" { if classRe, err = regexp.Compile(classExpr); err != nil { return nil, fmt.Errorf("class is a regular expression: %w", err) } } if nameExpr != "" { if nameRe, err = regexp.Compile(nameExpr); err != nil { return nil, fmt.Errorf("name is a regular expression: %w", err) } } res, s, err := x.d.InSession(ctx, "xwininfo", "-root", "-tree") if err != nil { return nil, err } if !res.OK() { if res.Code == 127 { return nil, fmt.Errorf("xwininfo is not installed here; the xorg module's package resource (xorg-xwininfo) installs it") } return nil, res.Err() } tree := ParseTree(res.Stdout) var shown []Window for _, w := range tree { switch { case classRe != nil && !(classRe.MatchString(w.Class) || classRe.MatchString(w.Instance)): continue case nameRe != nil && !nameRe.MatchString(w.Name): continue case classRe == nil && nameRe == nil && !all && w.Class == "": // Without a filter, the windows a program owns: those with a class. continue } shown = append(shown, w) } detailed := 0 if detail || classRe != nil || nameRe != nil { env := s.Env(x.d.Base) for i := range shown { if detailed == mostDetailed { break } detailed++ w := &shown[i] info := x.d.Run(ctx, env, nil, "xwininfo", "-id", w.ID, "-all") if !info.OK() { // A window can go between the tree and its details. w.DetailProblems = strings.TrimSpace(info.Stderr) continue } ParseDetails(w, info.Stdout) if props := x.d.Run(ctx, env, nil, "xprop", "-id", w.ID, "WM_WINDOW_ROLE", "_NET_WM_WINDOW_OPACITY"); props.OK() { ParseWindowProps(w, props.Stdout) } } } answer := map[string]any{"session": s.Display, "windows": shown, "count": len(shown), "in_tree": len(tree)} if detailed < len(shown) && (detail || classRe != nil || nameRe != nil) { answer["detailed"] = fmt.Sprintf("the first %d of %d; narrow class or name for the rest", detailed, len(shown)) } return answer, nil } // windowGeometry is a window's absolute position and size, from `xwininfo -id`. func (x xorg) windowGeometry(ctx context.Context, env []string, id string) (image4, error) { r := x.d.Run(ctx, env, nil, "xwininfo", "-id", id) if !r.OK() { if r.Code == 127 { return image4{}, fmt.Errorf("xwininfo is not installed here; the xorg module's package resource (xorg-xwininfo) installs it") } return image4{}, r.Err() } return ParseGeometry(r.Stdout) } // image4 is a rectangle on the screen, and the window's map state there. type image4 struct { X, Y, W, H int Mapped string } // ParseGeometry reads the absolute corner and the size from `xwininfo -id`. func ParseGeometry(text string) (image4, error) { var g image4 found := 0 for _, raw := range strings.Split(text, "\n") { line := strings.TrimSpace(raw) for prefix, into := range map[string]*int{ "Absolute upper-left X:": &g.X, "Absolute upper-left Y:": &g.Y, "Width:": &g.W, "Height:": &g.H, } { if strings.HasPrefix(line, "Map State:") { g.Mapped = strings.TrimSpace(strings.TrimPrefix(line, "Map State:")) } if strings.HasPrefix(line, prefix) { if n, err := strconv.Atoi(strings.TrimSpace(strings.TrimPrefix(line, prefix))); err == nil { *into = n found++ } } } } if found < 4 || g.W <= 0 || g.H <= 0 { return image4{}, fmt.Errorf("xwininfo gave no geometry for the window") } return g, nil }