28 KiB
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>
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 stoppour 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 switchutilise du fuzzy match :vmux switch authmatche la premiere session contenant "auth" dans branche git, label ou cwd. - D-08:
vmux label <session> "texte"attribue un label humain. Le label est stocke en memoire dans vmuxd (persiste sur disque dans~/.vmux/labels.jsonpour survivre aux redemarrages). - D-09: Le daemon track les transitions d'etat. Quand une session passe a NeedsInput, il enregistre le timestamp.
vmux listaffiche "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 </user_constraints>
<phase_requirements>
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. |
| </phase_requirements> |
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:
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).
// 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.
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
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<number>" line
}
Pattern 4: Workspace Switch via i3 IPC
What: Basculer vers le workspace d'une session.
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.
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.
// 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.
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
// 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
// Source: verification directe sur /proc/<PID>/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)
// 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
// 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/reponsesworkspace_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 :
readPPIDmockable 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
-
Concurrence X11 + i3 IPC dans le daemon
- Ce qu'on sait :
go.i3wm.org/i3/v4est concurrent-safe.xgbutiln'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.WmPidGetque dans la goroutine de scan (jamais depuis les handlers de requetes). Le scan produit une snapshotmap[int]stringatomiquement remplacee viasync.RWMutex.
- Ce qu'on sait :
-
Sous-commande
daemonvisible ou cachee ?- Ce qu'on sait : D-03 dit autostart implicite.
- Ce qui est flou : L'utilisateur peut-il lancer
vmux daemonmanuellement ? - Recommandation : Sous-commande cachee (non affichee dans l'aide). Utile pour le debug (
vmux daemon --foreground).
-
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 - API complete : Node struct (pas de PID), GetTree, GetWorkspaces, RunCommand, Subscribe
- BurntSushi/xgbutil/ewmh source -
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
piddans les nodes (14 windows verifiees)
Secondary (MEDIUM confidence)
- i3 IPC documentation - Protocole IPC officiel
- Go Unix Domain Sockets (Eli Bendersky) - Pattern daemon Go
- go-i3 GitHub - Repo officiel de la lib Go i3
Tertiary (LOW confidence)
- Comportement de
xgbutilen 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)