package main import ( "errors" "fmt" "math" "os" "path/filepath" "regexp" "strconv" "strings" "syscall" "time" ) // unit is the name picom runs under in the account's service manager when a tool starts it. The // session start runs its own, and a tool that starts one first ends any other (Toggle, Restart). const unit = "picom" func configPath() string { return filepath.Join(operatorHome(), ".config", "picom", "picom.conf") } // Running is picom's processes of this account. func running() []int { return processesOf("picom") } // RestartResult is what picom_restart answers. type RestartResult struct { Action string `json:"action"` PIDs []int `json:"pids"` Note string `json:"note,omitempty"` } // Restart tells a running picom to reinitialise, or starts one. func Restart(hard bool) (RestartResult, error) { if pids := running(); len(pids) > 0 && !hard { return RestartResult{Action: "reinitialised", PIDs: signalAll("picom", syscall.SIGUSR1), Note: "picom re-read " + configPath()}, nil } s, err := findSession() if err != nil { return RestartResult{}, err } if err := stopAll(); err != nil { return RestartResult{}, err } if err := s.detach(unit, "picom", "--config", configPath()); err != nil { return RestartResult{}, err } pids := waitFor(func() []int { return running() }, 3*time.Second) return RestartResult{Action: "started", PIDs: pids, Note: "started under the account's service manager as " + unit + ".service"}, nil } // stopAll ends every picom of this account and waits for them to go. func stopAll() error { signalAll("picom", syscall.SIGTERM) deadline := time.Now().Add(3 * time.Second) for time.Now().Before(deadline) { if len(running()) == 0 { return nil } time.Sleep(100 * time.Millisecond) } if left := running(); len(left) > 0 { return fmt.Errorf("picom %v did not end within 3s", left) } return nil } func waitFor(get func() []int, within time.Duration) []int { deadline := time.Now().Add(within) for { if got := get(); len(got) > 0 || time.Now().After(deadline) { return got } time.Sleep(100 * time.Millisecond) } } // ToggleResult is what picom_toggle answers. type ToggleResult struct { Compositing bool `json:"compositing"` PIDs []int `json:"pids"` Note string `json:"note"` } // Toggle stops or starts compositing; want nil flips it. func Toggle(want *bool) (ToggleResult, error) { on := len(running()) == 0 if want != nil { on = *want } if !on { if err := stopAll(); err != nil { return ToggleResult{}, err } return ToggleResult{Compositing: false, PIDs: []int{}, Note: "picom ended; the session start runs it again at the next login, or call picom_toggle with on"}, nil } if pids := running(); len(pids) > 0 { return ToggleResult{Compositing: true, PIDs: pids, Note: "picom was already running"}, nil } r, err := Restart(true) if err != nil { return ToggleResult{}, err } return ToggleResult{Compositing: len(r.PIDs) > 0, PIDs: r.PIDs, Note: r.Note}, nil } // Rule is one window rule, its options as written. type Rule struct { Match string `json:"match"` Options map[string]string `json:"options"` } // RulesResult is what picom_rules answers. type RulesResult struct { File string `json:"file"` Running []int `json:"running"` Global map[string]string `json:"global"` Rules []Rule `json:"rules"` // Legacy names options that window rules supersede, which picom ignores when rules are set. Legacy []string `json:"legacy,omitempty"` } var ( commentLine = regexp.MustCompile(`(?m)^\s*#.*$`) rulesBlock = regexp.MustCompile(`(?s)\brules\s*[=:]\s*\((.*?)\)\s*;`) ruleGroup = regexp.MustCompile(`(?s)\{(.*?)\}`) assignment = regexp.MustCompile(`(?m)^\s*([A-Za-z][A-Za-z0-9-]*)\s*[=:]\s*(.+?)\s*;?\s*$`) ) // superseded are the options picom's window rules replace (picom(1), WINDOW RULES). var superseded = []string{"opacity-rule", "inactive-opacity", "active-opacity", "inactive-dim", "shadow-exclude", "fade-exclude", "focus-exclude", "rounded-corners-exclude", "blur-background-exclude", "corner-radius-rules", "wintypes", "inactive-opacity-override", "mark-wmwin-focused"} // Rules reads picom's configuration file into its global options and its window rules. func Rules(path string) (RulesResult, error) { raw, err := os.ReadFile(path) if err != nil { return RulesResult{}, fmt.Errorf("picom's configuration: %w", err) } return parseRules(path, string(raw), running()), nil } func parseRules(path, conf string, pids []int) RulesResult { out := RulesResult{File: path, Running: pids, Global: map[string]string{}, Rules: []Rule{}} conf = commentLine.ReplaceAllString(conf, "") rest := conf if m := rulesBlock.FindStringSubmatchIndex(conf); m != nil { body := conf[m[2]:m[3]] rest = conf[:m[0]] + conf[m[1]:] for _, g := range ruleGroup.FindAllStringSubmatch(body, -1) { r := Rule{Options: map[string]string{}} for _, part := range strings.Split(g[1], ";") { k, v, ok := strings.Cut(part, "=") if !ok { continue } k, v = strings.TrimSpace(k), strings.TrimSpace(v) if k == "match" { r.Match = unquote(v) } else { r.Options[k] = unquote(v) } } out.Rules = append(out.Rules, r) } } for _, m := range assignment.FindAllStringSubmatch(rest, -1) { out.Global[m[1]] = unquote(strings.TrimSuffix(m[2], ";")) } if len(out.Rules) > 0 { for _, name := range superseded { if _, set := out.Global[name]; set { out.Legacy = append(out.Legacy, name) } } } return out } func unquote(v string) string { v = strings.TrimSpace(v) if len(v) >= 2 && v[0] == '"' && v[len(v)-1] == '"' { return v[1 : len(v)-1] } return v } // OpacityResult is what picom_window_opacity answers. type OpacityResult struct { Window string `json:"window"` Class string `json:"class,omitempty"` Title string `json:"title,omitempty"` // Opacity is the window's own value in percent; nil when it has none. Opacity *int `json:"opacity"` Note string `json:"note,omitempty"` } var ( activeWindow = regexp.MustCompile(`window id # (0x[0-9a-fA-F]+)`) cardinal = regexp.MustCompile(`_NET_WM_WINDOW_OPACITY\(CARDINAL\) = (\d+)`) wmClass = regexp.MustCompile(`WM_CLASS\(STRING\) = "[^"]*", "([^"]*)"`) wmName = regexp.MustCompile(`_NET_WM_NAME\(UTF8_STRING\) = "(.*)"`) ) // WindowOpacity reads a window's own opacity, and sets it when percent is 0-100. func WindowOpacity(window string, percent int) (OpacityResult, error) { s, err := findSession() if err != nil { return OpacityResult{}, err } if window == "" { r, err := s.run(5*time.Second, "", "xprop", "-root", "_NET_ACTIVE_WINDOW") if err != nil { return OpacityResult{}, err } m := activeWindow.FindStringSubmatch(r.Stdout) if m == nil || m[1] == "0x0" { return OpacityResult{}, errors.New("no window is focused") } window = m[1] } id, err := windowID(window) if err != nil { return OpacityResult{}, err } if percent >= 0 { var r Result if percent == 100 { r, err = s.run(5*time.Second, "", "xprop", "-id", id, "-remove", "_NET_WM_WINDOW_OPACITY") } else { r, err = s.run(5*time.Second, "", "xprop", "-id", id, "-f", "_NET_WM_WINDOW_OPACITY", "32c", "-set", "_NET_WM_WINDOW_OPACITY", strconv.FormatUint(opacityCardinal(percent), 10)) } if err != nil { return OpacityResult{}, err } if r.Code != 0 { return OpacityResult{}, fmt.Errorf("xprop: %s", strings.TrimSpace(r.Stderr)) } } r, err := s.run(5*time.Second, "", "xprop", "-id", id, "_NET_WM_WINDOW_OPACITY", "WM_CLASS", "_NET_WM_NAME") if err != nil { return OpacityResult{}, err } if r.Code != 0 { return OpacityResult{}, fmt.Errorf("window %s: %s", id, strings.TrimSpace(r.Stderr)) } out := parseWindow(id, r.Stdout) if rules, err := Rules(configPath()); err == nil && out.Class != "" { for _, rule := range rules.Rules { if _, sets := rule.Options["opacity"]; sets && strings.Contains(rule.Match, "class_g = '"+out.Class+"'") { out.Note = "a window rule sets the opacity of class " + out.Class + " and outranks the window's own value" } } } return out, nil } func windowID(w string) (string, error) { w = strings.TrimSpace(strings.ToLower(w)) base := 10 if strings.HasPrefix(w, "0x") { w, base = w[2:], 16 } n, err := strconv.ParseUint(w, base, 32) if err != nil || n == 0 { return "", fmt.Errorf("window %q is not an X window id", w) } return fmt.Sprintf("0x%x", n), nil } func opacityCardinal(percent int) uint64 { return uint64(math.Round(float64(percent) / 100 * float64(math.MaxUint32))) } func parseWindow(id, out string) OpacityResult { r := OpacityResult{Window: id} if m := cardinal.FindStringSubmatch(out); m != nil { n, _ := strconv.ParseUint(m[1], 10, 64) p := int(math.Round(float64(n) / float64(math.MaxUint32) * 100)) r.Opacity = &p } if m := wmClass.FindStringSubmatch(out); m != nil { r.Class = m[1] } if m := wmName.FindStringSubmatch(out); m != nil { r.Title = m[1] } return r }