diff --git a/workspace.go b/workspace.go index 0d8598f..19adca9 100644 --- a/workspace.go +++ b/workspace.go @@ -1,9 +1,18 @@ package main import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + i3 "go.i3wm.org/i3/v4" ) +const maxPPIDDepth = 20 + // X11PIDResolver abstrait la lecture de _NET_WM_PID pour testabilite. type X11PIDResolver interface { GetPID(windowID int64) (int, error) @@ -16,17 +25,77 @@ type I3TreeProvider interface { // ReadPPID lit le PPid depuis /proc/PID/status. procDir injectable pour les tests. func ReadPPID(procDir string, pid int) (int, error) { - return 0, nil + statusPath := filepath.Join(procDir, strconv.Itoa(pid), "status") + f, err := os.Open(statusPath) + if err != nil { + return 0, err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "PPid:") { + fields := strings.Fields(line) + if len(fields) < 2 { + return 0, fmt.Errorf("malformed PPid line: %q", line) + } + return strconv.Atoi(fields[1]) + } + } + return 0, fmt.Errorf("PPid not found in %s", statusPath) } // ResolveWorkspace remonte la chaine PPID depuis claudePID jusqu'a trouver -// un PID connu dans terminalWorkspaces. Max 20 niveaux. +// un PID connu dans terminalWorkspaces. Max 20 niveaux pour eviter les boucles. func ResolveWorkspace(procDir string, claudePID int, terminalWorkspaces map[int]string) string { + current := claudePID + for depth := 0; depth < maxPPIDDepth; depth++ { + ppid, err := ReadPPID(procDir, current) + if err != nil || ppid <= 0 { + return "" + } + if ws, ok := terminalWorkspaces[ppid]; ok { + return ws + } + current = ppid + } return "" } -// BuildTerminalWorkspaceMap construit la map terminalPID -> workspaceName. -// Utilise i3 GetTree + X11 _NET_WM_PID. +// BuildTerminalWorkspaceMap construit la map terminalPID -> workspaceName +// en parcourant l'arbre i3 et en resolvant le PID via X11 _NET_WM_PID. func BuildTerminalWorkspaceMap(tree I3TreeProvider, x11 X11PIDResolver) (map[int]string, error) { - return nil, nil + t, err := tree.GetTree() + if err != nil { + return nil, err + } + + result := make(map[int]string) + walkI3Tree(t.Root, "", x11, result) + return result, nil +} + +func walkI3Tree(node *i3.Node, currentWorkspace string, x11 X11PIDResolver, result map[int]string) { + if node == nil { + return + } + + if node.Type == i3.WorkspaceNode { + currentWorkspace = node.Name + } + + if node.Window > 0 && currentWorkspace != "" { + pid, err := x11.GetPID(node.Window) + if err == nil && pid > 0 { + result[pid] = currentWorkspace + } + } + + for _, child := range node.Nodes { + walkI3Tree(child, currentWorkspace, x11, result) + } + for _, child := range node.FloatingNodes { + walkI3Tree(child, currentWorkspace, x11, result) + } } diff --git a/x11_resolver.go b/x11_resolver.go new file mode 100644 index 0000000..a5d7f00 --- /dev/null +++ b/x11_resolver.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + "os" + + "github.com/BurntSushi/xgb/xproto" + "github.com/BurntSushi/xgbutil" + "github.com/BurntSushi/xgbutil/ewmh" +) + +// RealX11Resolver lit _NET_WM_PID via la connexion X11. +type RealX11Resolver struct { + xu *xgbutil.XUtil +} + +// NewRealX11Resolver ouvre une connexion X11. Retourne une erreur si $DISPLAY est absent. +func NewRealX11Resolver() (*RealX11Resolver, error) { + if os.Getenv("DISPLAY") == "" { + return nil, fmt.Errorf("DISPLAY not set, cannot resolve X11 PIDs") + } + xu, err := xgbutil.NewConn() + if err != nil { + return nil, fmt.Errorf("X11 connection failed: %w", err) + } + return &RealX11Resolver{xu: xu}, nil +} + +func (r *RealX11Resolver) GetPID(windowID int64) (int, error) { + pid, err := ewmh.WmPidGet(r.xu, xproto.Window(windowID)) + if err != nil { + return 0, err + } + return int(pid), nil +} + +// Close ferme la connexion X11. +func (r *RealX11Resolver) Close() { + if r.xu != nil { + r.xu.Conn().Close() + } +}