# Phase 2: Daemon et i3 Bridge - Research **Researched:** 2026-03-23 **Domain:** Go daemon (Unix socket IPC) + i3 IPC + X11 PID resolution **Confidence:** HIGH ## Summary La Phase 2 transforme vmux d'un CLI one-shot en un daemon persistant (vmuxd) qui maintient un registre des sessions a jour. Le daemon communique avec le CLI via un Unix socket JSON. L'integration i3 permet de mapper chaque session Claude Code a son workspace et de switcher en une action. **Decouverte critique :** i3 GetTree() ne contient PAS de champ PID dans ses nodes. Le contexte (D-04) suppose que "i3 GetTree() donne le PID de chaque fenetre terminal", ce qui est faux. Le champ `Window` (X11 window ID) est present, et il faut lire `_NET_WM_PID` via X11 pour obtenir le PID du terminal. La bonne nouvelle : `go.i3wm.org/i3/v4` depend deja de `BurntSushi/xgbutil` qui expose `ewmh.WmPidGet()`. Aucune dependance supplementaire. L'algorithme de mapping est : i3 GetTree() -> Node.Window -> `ewmh.WmPidGet()` -> terminal PID -> walk up /proc PPID chain depuis chaque Claude PID jusqu'a trouver un terminal PID connu. **Recommandation principale :** Utiliser `go.i3wm.org/i3/v4` + `BurntSushi/xgbutil/ewmh` (deja en transitive dep) pour le mapping PID-workspace. Pas de shell-out vers xdotool/xprop. ## User Constraints (from CONTEXT.md) ### Locked Decisions - **D-01:** vmuxd communique avec le CLI via un Unix socket (`~/.vmux/vmux.sock`). Protocole JSON sur le socket. - **D-02:** vmuxd poll /proc + JSONL toutes les 5 secondes pour mettre a jour le registre des sessions. La Phase 3 (hooks) reduira la latence a < 1s. - **D-03:** Autostart : `vmux list` (ou tout autre commande) demarre vmuxd automatiquement s'il ne tourne pas. `vmux stop` pour arreter proprement. - **D-04:** Mapping session -> workspace via le PID. i3 GetTree() donne le PID de chaque fenetre terminal ; on croise avec le PID du processus Claude Code pour trouver le workspace. **NOTE RECHERCHE : i3 GetTree() n'a PAS de champ PID. Utiliser Node.Window + _NET_WM_PID via X11. Meme resultat final.** - **D-05:** Si plusieurs sessions Claude Code sont dans le meme workspace i3, elles sont toutes listees avec le meme workspace. - **D-06:** Utiliser `go.i3wm.org/i3/v4` (lib officielle Go pour i3 IPC). Premiere dependance externe du projet. API : GetTree, GetWorkspaces, RunCommand. - **D-07:** `vmux switch` utilise du fuzzy match : `vmux switch auth` matche la premiere session contenant "auth" dans branche git, label ou cwd. - **D-08:** `vmux label "texte"` attribue un label humain. Le label est stocke en memoire dans vmuxd (persiste sur disque dans `~/.vmux/labels.json` pour survivre aux redemarrages). - **D-09:** Le daemon track les transitions d'etat. Quand une session passe a NeedsInput, il enregistre le timestamp. `vmux list` affiche "depuis X min". ### Claude's Discretion - Format de communication JSON sur le Unix socket (structure des requetes/reponses) - Gestion du PID file pour le daemon - Gestion des erreurs de connexion i3 IPC (fallback gracieux si i3 non disponible) ### Deferred Ideas (OUT OF SCOPE) None ## Phase Requirements | ID | Description | Research Support | |----|-------------|------------------| | DISC-04 | vmux permet d'associer un label humain a une session | Labels stockes en memoire + persistes dans `~/.vmux/labels.json`. Protocole socket : requete `label` avec session_id + texte. | | I3-01 | vmux mappe chaque session a son workspace i3 | Algorithme : i3 GetTree -> Node.Window -> ewmh.WmPidGet -> terminal PID -> PPID chain matching. Verifie sur machine : 6 Claude PIDs correctement mappes a leurs workspaces. | | I3-02 | vmux permet de switcher vers le workspace i3 d'une session en une action | `i3.RunCommand("workspace number N")` via go.i3wm.org/i3/v4. Fuzzy match sur label/branche/cwd pour trouver la session. | | STATE-04 | vmux affiche le temps ecoule depuis que la session attend | Daemon track les transitions d'etat avec timestamp. Quand state passe a NeedsInput, enregistre `WaitingSince`. Affichage "depuis X min" calcule par le CLI. | ## Project Constraints (from CLAUDE.md) - NixOS : dependances via `nix-shell` - Code simple, expressif, bien concu - Commentaires uniquement pour le "Pourquoi" - TDD Chicago School - Ne jamais modifier les .env - Francais avec accents dans les communications ## Standard Stack ### Core | Library | Version | Purpose | Why Standard | |---------|---------|---------|--------------| | Go | 1.25.7 | Langage | Deja utilise en Phase 1. Disponible via `nix-shell -p go`. | | go.i3wm.org/i3/v4 | v4.24.0 | i3 IPC (GetTree, GetWorkspaces, RunCommand) | Lib officielle Go pour i3. Couvre 100% du protocole IPC. Retry transparent sur restart i3. | | BurntSushi/xgbutil/ewmh | v0.0.0-20190907113008 | Lecture _NET_WM_PID des fenetres X11 | Dependance transitive de go.i3wm.org/i3/v4. Pas d'ajout explicite. `ewmh.WmPidGet(xu, win)` retourne le PID. | | Go stdlib (net, encoding/json, os, sync) | - | Unix socket, JSON protocol, /proc, concurrence | Daemon et IPC ne necessitent aucune lib externe. | ### Supporting | Library | Version | Purpose | When to Use | |---------|---------|---------|-------------| | BurntSushi/xgb | v0.0.0-20210121224620 | Connexion X11 bas niveau | Dependance transitive. Necessaire pour initialiser xgbutil. | ### Alternatives Considered | Instead of | Could Use | Tradeoff | |------------|-----------|----------| | BurntSushi/xgbutil pour _NET_WM_PID | exec xdotool | 37ms pour 14 fenetres, acceptable mais shell-out inutile puisque xgbutil est deja en dep transitive | | stdlib net (Unix socket) | gRPC / protobuf | Over-engineered pour un daemon local avec 4 requetes | | stdlib flag avec sous-commandes manuelles | cobra | Over-engineered, 4 sous-commandes max | | PID file maison | github.com/nightlyone/lockfile | Trop simple pour justifier une dep (un os.WriteFile + flock) | **Installation:** ```bash nix-shell -p go --run "go get go.i3wm.org/i3/v4@v4.24.0" ``` **Version verification:** `go.i3wm.org/i3/v4 v4.24.0` correspond a i3 4.24 installe sur la machine. `BurntSushi/xgb` et `xgbutil` sont des dependances transitives, pas d'ajout explicite dans go.mod. ## Architecture Patterns ### Recommended Project Structure ``` vmux/ ├── main.go # Point d'entree CLI, dispatch sous-commandes, autostart daemon ├── daemon.go # vmuxd : boucle de poll, registre en memoire, Unix socket server ├── daemon_test.go ├── client.go # Client Unix socket (CLI -> daemon) ├── client_test.go ├── protocol.go # Types requete/reponse JSON du socket ├── protocol_test.go ├── i3bridge.go # Integration i3 : GetTree, PID resolution, RunCommand ├── i3bridge_test.go ├── workspace.go # Mapping PID -> workspace via PPID chain + _NET_WM_PID ├── workspace_test.go ├── proc.go # (Phase 1, inchange) ├── proc_test.go ├── session.go # (Phase 1, inchange) ├── session_test.go ├── state.go # (Phase 1, inchange) ├── state_test.go ├── display.go # Etendu : workspace, label, temps d'attente ├── display_test.go ├── types.go # Enrichi : Workspace, Label, WaitingSince dans Session ├── go.mod ├── go.sum └── shell.nix ``` ### Pattern 1: Daemon avec Unix Socket **What:** vmuxd ecoute sur `~/.vmux/vmux.sock`, recoit des requetes JSON, repond avec le registre. **When to use:** Toutes les commandes CLI (`list`, `switch`, `label`, `stop`). ```go // Protocole JSON simple : une requete = un objet JSON, une reponse = un objet JSON // Delimiter : chaque message termine par \n (JSON Lines sur le socket) type Request struct { Action string `json:"action"` // "list", "switch", "label", "stop" Args json.RawMessage `json:"args,omitempty"` } type Response struct { OK bool `json:"ok"` Error string `json:"error,omitempty"` Sessions []SessionInfo `json:"sessions,omitempty"` } type SessionInfo struct { PID int `json:"pid"` SessionID string `json:"session_id"` Cwd string `json:"cwd"` GitBranch string `json:"git_branch"` State string `json:"state"` Preview string `json:"preview"` Workspace string `json:"workspace"` Label string `json:"label,omitempty"` WaitingSince *time.Time `json:"waiting_since,omitempty"` } ``` ### Pattern 2: Autostart Daemon **What:** Le CLI tente de se connecter au socket. Si echec, il lance vmuxd en arriere-plan puis re-essaie. **When to use:** A chaque commande CLI. ```go func ensureDaemon(sockPath string) error { conn, err := net.DialTimeout("unix", sockPath, 500*time.Millisecond) if err == nil { conn.Close() return nil // daemon deja actif } // Lancer le daemon en background exe, _ := os.Executable() cmd := exec.Command(exe, "daemon") cmd.Stdout = nil cmd.Stderr = nil cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} // detacher du terminal if err := cmd.Start(); err != nil { return fmt.Errorf("cannot start daemon: %w", err) } // Attendre que le socket soit pret (retry avec backoff) for i := 0; i < 20; i++ { time.Sleep(50 * time.Millisecond) conn, err := net.DialTimeout("unix", sockPath, 200*time.Millisecond) if err == nil { conn.Close() return nil } } return fmt.Errorf("daemon did not start in time") } ``` ### Pattern 3: PID -> Workspace Resolution (CORRIGE vs D-04) **What:** Mapper un PID Claude Code a son workspace i3. **Algorithme verifie sur la machine :** ``` 1. i3.GetTree() -> arbre de nodes 2. Pour chaque node avec Window > 0 : a. ewmh.WmPidGet(xu, xproto.Window(node.Window)) -> terminal PID b. Stocker dans map[terminalPID]workspaceName 3. Pour chaque Claude PID : a. Lire /proc/PID/status -> PPid b. Remonter la chaine PPID jusqu'a trouver un terminalPID dans la map c. Le workspace correspondant est le workspace de la session ``` **Verification reelle sur la machine :** | Claude PID | Terminal PID (ancestor) | Workspace | |------------|------------------------|-----------| | 10466 | 10138 | 1 | | 13736 | 11864 | 10 | | 148021 | 147724 | 2 | | 16030 | 15281 | 10 | | 179544 | 178345 | 3 | | 87142 | 86863 | 3 | Chaine typique : `claude -> zsh -> .sakura-wrapped -> i3` ```go func resolveWorkspace(claudePID int, terminalWorkspaces map[int]string) string { pid := claudePID for i := 0; i < 20; i++ { // max 20 ancestors (securite) if ws, ok := terminalWorkspaces[pid]; ok { return ws } ppid, err := readPPID(pid) if err != nil || ppid <= 1 { return "" // pas de workspace trouve } pid = ppid } return "" } func readPPID(pid int) (int, error) { data, err := os.ReadFile(fmt.Sprintf("/proc/%d/status", pid)) // Parse "PPid:\t" line } ``` ### Pattern 4: Workspace Switch via i3 IPC **What:** Basculer vers le workspace d'une session. ```go func switchToWorkspace(wsName string) error { results, err := i3.RunCommand(fmt.Sprintf("workspace number %s", wsName)) if err != nil { return err } if len(results) > 0 && !results[0].Success { return fmt.Errorf("i3: %s", results[0].Error) } return nil } ``` ### Pattern 5: Fuzzy Match pour Switch **What:** `vmux switch auth` matche la premiere session contenant "auth" dans label, branche, ou cwd. **Ordre de priorite :** label > branche git > dernier segment du cwd. ```go func fuzzyMatch(query string, sessions []SessionInfo) *SessionInfo { query = strings.ToLower(query) // Priorite 1: label contient query for _, s := range sessions { if strings.Contains(strings.ToLower(s.Label), query) { return &s } } // Priorite 2: branche git contient query for _, s := range sessions { if strings.Contains(strings.ToLower(s.GitBranch), query) { return &s } } // Priorite 3: cwd contient query for _, s := range sessions { if strings.Contains(strings.ToLower(s.Cwd), query) { return &s } } return nil } ``` ### Pattern 6: Labels Persistence **What:** Labels persistes dans `~/.vmux/labels.json`. ```go // Format: map[sessionID]string type LabelStore struct { mu sync.RWMutex labels map[string]string path string } func (ls *LabelStore) Set(sessionID, label string) error { ls.mu.Lock() defer ls.mu.Unlock() ls.labels[sessionID] = label return ls.save() } func (ls *LabelStore) save() error { data, _ := json.MarshalIndent(ls.labels, "", " ") return os.WriteFile(ls.path, data, 0o644) } ``` ### Pattern 7: Tracking des Transitions d'Etat **What:** Le daemon enregistre le timestamp quand une session passe a NeedsInput. ```go type SessionRegistry struct { mu sync.RWMutex sessions map[string]*TrackedSession // key = sessionID } type TrackedSession struct { SessionInfo PrevState string WaitingSince time.Time // set quand state passe a NeedsInput } func (r *SessionRegistry) Update(info SessionInfo) { r.mu.Lock() defer r.mu.Unlock() existing, ok := r.sessions[info.SessionID] if !ok { r.sessions[info.SessionID] = &TrackedSession{SessionInfo: info} return } if info.State == "Needs Input" && existing.PrevState != "Needs Input" { existing.WaitingSince = time.Now() } existing.PrevState = existing.State existing.SessionInfo = info if info.State == "Needs Input" { info.WaitingSince = &existing.WaitingSince } } ``` ### Anti-Patterns to Avoid - **i3 GetTree() sans _NET_WM_PID :** Le Node i3 n'a PAS de champ PID. Il faut passer par X11 pour resoudre Window -> PID. - **Relancer le daemon a chaque commande :** Utiliser autostart (connect, si echec start, re-connect). - **Socket HTTP/REST :** Over-engineered. JSON Lines sur Unix socket suffit (une ligne = une requete, une ligne = une reponse). - **Polling i3 GetTree a chaque scan :** Coupler le scan /proc avec le scan i3 dans la meme boucle de 5s pour avoir une vue coherente. - **Stocker les labels par PID :** Les PID changent a chaque redemarrage de Claude Code. Utiliser le sessionID (UUID stable). ## Don't Hand-Roll | Problem | Don't Build | Use Instead | Why | |---------|-------------|-------------|-----| | i3 IPC protocol | Parser binaire i3 IPC | `go.i3wm.org/i3/v4` | Protocole binaire complexe avec magic bytes, types, longueurs. Retry transparent integre. | | X11 window properties | Shell-out vers xprop/xdotool | `BurntSushi/xgbutil/ewmh.WmPidGet()` | Deja en dep transitive. Pure Go, pas de fork/exec. | | Process tree walking | Lib procfs | `os.ReadFile("/proc/PID/status")` | Une seule ligne a parser (PPid). Trivial. | | JSON encoding/decoding | Lib serialisation | `encoding/json` stdlib | JSON Lines est trivial. | | CLI sub-commands | Framework CLI (cobra) | `os.Args` manual dispatch | 5 sous-commandes max (`list`, `switch`, `label`, `stop`, `daemon`). Un switch/case suffit. | **Insight :** La seule vraie complexite externe est i3 IPC (protocole binaire) et X11 (protocole bas niveau). Les deux sont couverts par les dependances de `go.i3wm.org/i3/v4`. ## Common Pitfalls ### Pitfall 1: i3 Node n'a pas de PID **What goes wrong:** Le code suppose que `Node.PID` existe (comme dit dans D-04) et panic/compile error. **Why it happens:** Confusion courante. i3 ne stocke pas le PID dans l'arbre. Le PID est une propriete X11 du client window. **How to avoid:** Utiliser `Node.Window` (X11 window ID) + `ewmh.WmPidGet()` pour obtenir le terminal PID. **Warning signs:** Champ inexistant dans la struct Node du Go package. ### Pitfall 2: Socket file residuel apres crash **What goes wrong:** vmuxd ne demarre pas car `~/.vmux/vmux.sock` existe deja (bind: address already in use). **Why it happens:** Crash ou kill -9 sans cleanup. **How to avoid:** Avant `net.Listen("unix", path)`, tenter un `net.Dial("unix", path)`. Si echec -> supprimer le fichier stale. Si succes -> daemon deja actif. **Warning signs:** "address already in use" au demarrage. ### Pitfall 3: Race condition daemon/CLI **What goes wrong:** Le CLI envoie une requete avant que le daemon ait fini son premier scan. **Why it happens:** Autostart : le daemon vient de demarrer, le registre est vide. **How to avoid:** Le daemon fait un scan synchrone AVANT d'ecouter sur le socket. Le CLI attend que le socket soit pret (retry 50ms x 20). **Warning signs:** `vmux list` retourne 0 sessions juste apres le premier lancement. ### Pitfall 4: i3 non disponible (SSH, TTY, Wayland) **What goes wrong:** `i3.GetTree()` echoue car i3 IPC n'est pas accessible. **Why it happens:** vmux est execute dans un environnement sans i3 (SSH, console, Sway/Wayland). **How to avoid:** Attraper l'erreur de `GetTree()`, mettre workspace="" pour toutes les sessions. `vmux list` fonctionne toujours, sans colonne workspace. `vmux switch` retourne une erreur claire. **Warning signs:** Erreur au premier scan. ### Pitfall 5: xgbutil necessita DISPLAY **What goes wrong:** `xgbutil.NewConn()` echoue si `$DISPLAY` n'est pas set. **Why it happens:** Meme situation que pitfall 4 : pas de X11. **How to avoid:** Verifier `$DISPLAY` avant d'initialiser xgbutil. Si absent, desactiver le mapping workspace (fallback gracieux). **Warning signs:** Erreur "cannot open display" au demarrage du daemon. ### Pitfall 6: Labels orphelins dans labels.json **What goes wrong:** labels.json grossit indefiniment avec des sessionIDs de sessions terminees. **Why it happens:** Les sessions Claude Code sont ephemeres, les sessionIDs changent. **How to avoid:** Au chargement de labels.json, ne garder que les labels dont le sessionID correspond a une session active. Ou purger periodiquement (ex: garder les 100 derniers). **Warning signs:** labels.json de plusieurs Mo apres des semaines d'utilisation. ### Pitfall 7: PPID chain walk infinie **What goes wrong:** La boucle de walk PPID ne termine pas (process circulaire theorique). **Why it happens:** Bug kernel theorique ou processus zombie. **How to avoid:** Limiter a 20 iterations. PID 1 (init) est toujours le point d'arret. **Warning signs:** CPU spike pendant le scan. ## Code Examples ### Initialisation X11 pour _NET_WM_PID ```go // Source: BurntSushi/xgbutil/ewmh package (dep transitive de go.i3wm.org/i3/v4) import ( "github.com/BurntSushi/xgb/xproto" "github.com/BurntSushi/xgbutil" "github.com/BurntSushi/xgbutil/ewmh" "go.i3wm.org/i3/v4" ) func buildTerminalWorkspaceMap() (map[int]string, error) { xu, err := xgbutil.NewConn() if err != nil { return nil, fmt.Errorf("X11 connection failed: %w", err) } defer xu.Conn().Close() tree, err := i3.GetTree() if err != nil { return nil, fmt.Errorf("i3 GetTree failed: %w", err) } result := make(map[int]string) var walk func(node *i3.Node, wsName string) walk = func(node *i3.Node, wsName string) { if node.Type == i3.WorkspaceNode { wsName = node.Name } if node.Window > 0 { pid, err := ewmh.WmPidGet(xu, xproto.Window(node.Window)) if err == nil && pid > 0 { result[int(pid)] = wsName } } for _, child := range node.Nodes { walk(child, wsName) } for _, child := range node.FloatingNodes { walk(child, wsName) } } walk(tree.Root, "") return result, nil } ``` ### Lecture du PPID depuis /proc ```go // Source: verification directe sur /proc//status func readPPID(pid int) (int, error) { data, err := os.ReadFile(fmt.Sprintf("/proc/%d/status", pid)) if err != nil { return 0, err } for _, line := range strings.Split(string(data), "\n") { if strings.HasPrefix(line, "PPid:") { fields := strings.Fields(line) if len(fields) >= 2 { return strconv.Atoi(fields[1]) } } } return 0, fmt.Errorf("PPid not found for PID %d", pid) } ``` ### Unix Socket Server (daemon) ```go // Source: Go stdlib net package func startDaemonSocket(sockPath string) (net.Listener, error) { // Cleanup stale socket if _, err := net.DialTimeout("unix", sockPath, 200*time.Millisecond); err != nil { os.Remove(sockPath) // stale, supprimer } os.MkdirAll(filepath.Dir(sockPath), 0o700) return net.Listen("unix", sockPath) } func serveDaemon(listener net.Listener, registry *SessionRegistry) { for { conn, err := listener.Accept() if err != nil { return // listener ferme } go handleConnection(conn, registry) } } func handleConnection(conn net.Conn, registry *SessionRegistry) { defer conn.Close() decoder := json.NewDecoder(conn) encoder := json.NewEncoder(conn) var req Request if err := decoder.Decode(&req); err != nil { return } switch req.Action { case "list": sessions := registry.List() encoder.Encode(Response{OK: true, Sessions: sessions}) case "switch": // ... fuzzy match + i3.RunCommand case "label": // ... set label case "stop": encoder.Encode(Response{OK: true}) // signal shutdown } } ``` ### Switch workspace via i3 ```go // Source: go.i3wm.org/i3/v4 documentation (pkg.go.dev) func switchToWorkspace(wsName string) error { cmd := fmt.Sprintf("workspace number %s", wsName) results, err := i3.RunCommand(cmd) if err != nil { if i3.IsUnsuccessful(err) { return fmt.Errorf("i3 switch failed: %v", results[0].Error) } return err } return nil } ``` ## State of the Art | Old Approach | Current Approach | When Changed | Impact | |--------------|------------------|--------------|--------| | BurntSushi/xgb (original) | jezek/xgb (maintained fork) | 2021 | jezek/xgb est le fork actif. Mais go.i3wm.org/i3/v4 utilise encore BurntSushi/xgb. | | i3 IPC v4.0 (basique) | i3 IPC v4.24 (complet) | 2024 | Memes APIs core. GetTree, GetWorkspaces, RunCommand stables depuis des annees. | ## Validation Architecture ### Test Framework | Property | Value | |----------|-------| | Framework | Go testing (stdlib) | | Config file | Aucun (convention Go par defaut) | | Quick run command | `go test ./...` | | Full suite command | `go test -v -race ./...` | ### Phase Requirements -> Test Map | Req ID | Behavior | Test Type | Automated Command | File Exists? | |--------|----------|-----------|-------------------|-------------| | DISC-04 | Label humain sur une session | unit | `go test -run TestLabelStore -v` | Wave 0 | | I3-01 | Mapping session -> workspace i3 | unit | `go test -run TestResolveWorkspace -v` | Wave 0 | | I3-02 | Switch vers workspace i3 d'une session | unit+integration | `go test -run TestFuzzyMatch -v` | Wave 0 | | STATE-04 | Temps ecoule depuis attente | unit | `go test -run TestWaitingSince -v` | Wave 0 | ### Sampling Rate - **Per task commit:** `go test ./...` - **Per wave merge:** `go test -v -race ./...` - **Phase gate:** Full suite green before `/gsd:verify-work` ### Wave 0 Gaps - [ ] `daemon_test.go` -- poll loop, registre, autostart logic (testable avec mock socket) - [ ] `client_test.go` -- requetes/reponses sur socket (testable avec mock server) - [ ] `protocol_test.go` -- serialisation JSON des requetes/reponses - [ ] `workspace_test.go` -- couvre I3-01 : PPID chain walk (testable avec fake /proc + mock i3 tree) - [ ] `i3bridge_test.go` -- couvre I3-02 : fuzzy match (pure logique, pas de mock) ### Strategy TDD Le daemon est testable en isolant les couches : - **Registre** : pure logique, pas de I/O. Tester les transitions d'etat, le tracking WaitingSince. - **Protocol** : serialisation/deserialisation JSON. Tester les request/response structs. - **Workspace resolution** : `readPPID` mockable via un fake /proc. Le mapping i3 mockable via une interface. - **Client/Server** : Tester avec un vrai Unix socket dans un tmpdir. - **i3 bridge** : Abstraire derriere une interface (`type I3Client interface { GetTree() ... }`). Mocker en test. L'integration i3 reelle (GetTree, RunCommand) sera testee manuellement sur la machine. Les tests unitaires couvrent toute la logique sans dependre de i3. ## Environment Availability | Dependency | Required By | Available | Version | Fallback | |------------|------------|-----------|---------|----------| | Go | Build & run | oui | 1.25.7 (via nix-shell) | - | | i3 | Workspace mapping + switch | oui | 4.24 | Fallback gracieux : vmux fonctionne sans workspace si i3 absent | | X11 (DISPLAY) | _NET_WM_PID resolution | oui | X11 actif (i3 tourne) | Meme fallback : pas de workspace sans X11 | | /proc filesystem | Process detection + PPID chain | oui | procfs Linux 6.19 | - | | xdotool | Non utilise (pure Go via xgbutil) | oui | 3.20211022.1 | - | **Missing dependencies with no fallback:** Aucune. **Missing dependencies with fallback:** - i3 + X11 : si absents, vmux fonctionne sans les colonnes workspace et sans `vmux switch`. Toutes les autres fonctionnalites (list, label, temps d'attente) restent operationnelles. ## Open Questions 1. **Concurrence X11 + i3 IPC dans le daemon** - Ce qu'on sait : `go.i3wm.org/i3/v4` est concurrent-safe. `xgbutil` n'est PAS concurrent-safe explicitement. - Ce qui est flou : Le daemon poll dans une goroutine et repond aux requetes dans d'autres. La connexion X11 doit-elle etre re-ouverte a chaque scan ou partagee ? - Recommandation : Ouvrir la connexion X11 une fois au demarrage du daemon. N'utiliser `ewmh.WmPidGet` que dans la goroutine de scan (jamais depuis les handlers de requetes). Le scan produit une snapshot `map[int]string` atomiquement remplacee via `sync.RWMutex`. 2. **Sous-commande `daemon` visible ou cachee ?** - Ce qu'on sait : D-03 dit autostart implicite. - Ce qui est flou : L'utilisateur peut-il lancer `vmux daemon` manuellement ? - Recommandation : Sous-commande cachee (non affichee dans l'aide). Utile pour le debug (`vmux daemon --foreground`). 3. **Nettoyage des sessions disparues** - Ce qu'on sait : Le daemon poll toutes les 5s. Si un processus Claude disparait, il ne sera plus dans /proc. - Ce qui est flou : Faut-il garder l'entree quelques secondes pour eviter le flicker ? - Recommandation : Supprimer immediatement du registre. Si le PID n'est plus dans /proc, la session est terminee. ## Sources ### Primary (HIGH confidence) - [go.i3wm.org/i3/v4 pkg.go.dev](https://pkg.go.dev/go.i3wm.org/i3/v4) - API complete : Node struct (pas de PID), GetTree, GetWorkspaces, RunCommand, Subscribe - [BurntSushi/xgbutil/ewmh source](https://github.com/BurntSushi/xgbutil/blob/master/ewmh/ewmh.go) - `WmPidGet(xu, win)` pour _NET_WM_PID - Inspection directe machine : 6 Claude PIDs mappes a leurs workspaces via PPID chain - i3-msg -t get_tree : confirme absence du champ `pid` dans les nodes (14 windows verifiees) ### Secondary (MEDIUM confidence) - [i3 IPC documentation](https://i3wm.org/docs/ipc.html) - Protocole IPC officiel - [Go Unix Domain Sockets (Eli Bendersky)](https://eli.thegreenplace.net/2019/unix-domain-sockets-in-go/) - Pattern daemon Go - [go-i3 GitHub](https://github.com/i3/go-i3) - Repo officiel de la lib Go i3 ### Tertiary (LOW confidence) - Comportement de `xgbutil` en multi-thread : documentation limitee, recommandation basee sur les bonnes pratiques Go (single writer) ## Metadata **Confidence breakdown:** - Standard stack: HIGH - go.i3wm.org/i3/v4 v4.24.0 verifie, BurntSushi/xgbutil confirme en dep transitive - Architecture: HIGH - Algorithme PID->workspace verifie sur 6 sessions reelles - Pitfalls: HIGH - Absence de PID dans Node i3 decouverte par inspection directe, pas par supposition - Protocol: MEDIUM - Design du protocole socket base sur des patterns Go standard, pas de source specifique **Research date:** 2026-03-23 **Valid until:** 2026-04-23 (i3 IPC stable depuis des annees, /proc stable)