// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package atspi import ( "fmt" "io" "os" "strings" "syscall" ) type flatpakInstance struct { application, instance string } // Flatpak gives its D-Bus proxy and application the same /.flatpak-info. // Compare a running instance, never just an application ID: two instances of // the same application must not acquire one another's foreground window. func readFlatpakInstance(procRoot string, pid int) flatpakInstance { if pid <= 0 { return flatpakInstance{} } file, err := os.OpenFile(fmt.Sprintf("%s/%d/root/.flatpak-info", procRoot, pid), os.O_RDONLY|syscall.O_NONBLOCK, 0) if err != nil { return flatpakInstance{} } defer file.Close() const limit = 64 * 1024 stat, err := file.Stat() if err != nil || !stat.Mode().IsRegular() || stat.Size() > limit { return flatpakInstance{} } data, err := io.ReadAll(io.LimitReader(file, limit+1)) if err != nil || len(data) > limit { return flatpakInstance{} } var result flatpakInstance var section string for _, line := range strings.Split(string(data), "\n") { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") { continue } if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") { section = line continue } key, value, ok := strings.Cut(line, "=") if !ok { return flatpakInstance{} } key, value = strings.TrimSpace(key), strings.TrimSpace(value) if section == "[Application]" && key == "name" { if result.application != "" { return flatpakInstance{} } result.application = value } if section == "[Instance]" && key == "instance-id" { if result.instance != "" { return flatpakInstance{} } result.instance = value } } if result.application == "" || result.instance == "" { return flatpakInstance{} } return result } func sameFlatpakInstance(procRoot string, first, second int) bool { identity := readFlatpakInstance(procRoot, first) return identity != (flatpakInstance{}) && identity == readFlatpakInstance(procRoot, second) } // Owned by the GLib thread and cleared with the foreground client. This only // resolves process identity; fresh window state and geometry still decide // whether an accessible can own focus. func (adapter *Adapter) foregroundProcessID(pid int) int { client := adapter.Foreground().Client if client.Window == 0 || client.PID <= 0 || pid <= 0 || pid == client.PID { return pid } matched, known := adapter.foregroundProcesses[pid] if !known { matched = sameFlatpakInstance("/proc", client.PID, pid) if adapter.foregroundProcesses == nil || len(adapter.foregroundProcesses) >= 128 { adapter.foregroundProcesses = make(map[int]bool) } adapter.foregroundProcesses[pid] = matched } if matched { return client.PID } return pid }