feat(02-02): implement PPID chain walk + workspace resolution

- ReadPPID parses PPid from /proc/PID/status
- ResolveWorkspace walks PPID chain (max 20 levels) to find terminal workspace
- BuildTerminalWorkspaceMap traverses i3 tree + X11 _NET_WM_PID
- RealX11Resolver wraps xgbutil/ewmh for production use
- Interfaces I3TreeProvider and X11PIDResolver for testability
- Fix unused imports in daemon.go (Rule 3: blocking build)
This commit is contained in:
Pierre Martin
2026-03-23 17:43:58 +01:00
parent 5315e88494
commit a2fb37e2b5
2 changed files with 116 additions and 5 deletions

View File

@@ -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)
}
}