Files
vmux/.planning/phases/02-daemon-et-i3-bridge/02-RESEARCH.md
2026-03-23 15:32:48 +01:00

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 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 <session> "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 </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

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/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 - 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 pid dans les nodes (14 windows verifiees)

Secondary (MEDIUM confidence)

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)