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:
79
workspace.go
79
workspace.go
@@ -1,9 +1,18 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
i3 "go.i3wm.org/i3/v4"
|
i3 "go.i3wm.org/i3/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const maxPPIDDepth = 20
|
||||||
|
|
||||||
// X11PIDResolver abstrait la lecture de _NET_WM_PID pour testabilite.
|
// X11PIDResolver abstrait la lecture de _NET_WM_PID pour testabilite.
|
||||||
type X11PIDResolver interface {
|
type X11PIDResolver interface {
|
||||||
GetPID(windowID int64) (int, error)
|
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.
|
// ReadPPID lit le PPid depuis /proc/PID/status. procDir injectable pour les tests.
|
||||||
func ReadPPID(procDir string, pid int) (int, error) {
|
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
|
// 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 {
|
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 ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildTerminalWorkspaceMap construit la map terminalPID -> workspaceName.
|
// BuildTerminalWorkspaceMap construit la map terminalPID -> workspaceName
|
||||||
// Utilise i3 GetTree + X11 _NET_WM_PID.
|
// 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) {
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
42
x11_resolver.go
Normal file
42
x11_resolver.go
Normal file
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user