diff --git a/.planning/phases/04-notifications-et-i3bar/04-RESEARCH.md b/.planning/phases/04-notifications-et-i3bar/04-RESEARCH.md new file mode 100644 index 0000000..ef98d62 --- /dev/null +++ b/.planning/phases/04-notifications-et-i3bar/04-RESEARCH.md @@ -0,0 +1,457 @@ +# Phase 4: Notifications et i3bar - Research + +**Researched:** 2026-03-24 +**Domain:** Desktop notifications (D-Bus/dunst), i3bar protocol, daemon state management +**Confidence:** HIGH + +## Summary + +Cette phase ajoute trois fonctionnalites au daemon vmux : (1) notifications dunst lors de transitions Working vers Needs Input, (2) mode focus avec timer pour supprimer les notifications, (3) widget i3bar affichant le statut des sessions. + +L'approche recommandee utilise `notify-send` via `os/exec` pour les notifications (simple, zero dependance, dunst est deja installe), un script standalone pour le widget i3bar qui query le daemon via le socket Unix, et un timer en memoire dans le daemon pour le mode focus. + +**Primary recommendation:** `notify-send` pour les notifications, script i3bar standalone qui remplace i3status dans la config i3, focus timer en memoire dans le Daemon struct. + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions +- **D-01:** Notifier uniquement sur transition Working -> Needs Input. Pas de notif pour Idle -> Needs Input ou Working -> Idle. +- **D-02:** Pas de debounce en v1. Un event hook = une transition = une notification (si pas en mode focus). +- **D-03:** Envoyer les notifications via `notify-send` (ou esiqveland/notify D-Bus). Claude decide l'approche. +- **D-04:** Timer uniquement : `vmux focus 30` (30 min). Se desactive automatiquement apres la duree. Pas de toggle on/off sans duree. +- **D-05:** Le mode focus supprime les notifications dunst. Le widget i3bar reste visible. +- **D-06:** Format liste courte : `vmux: auth[!] portal[W] neia[I]`. Les noms sont le dernier segment du cwd ou le label si defini. `[!]` = Needs Input, `[W]` = Working, `[I]` = Idle. +- **D-07:** Quand aucune session n'attend : `vmux: all working (3)`. +- **D-08:** Couleurs selon urgence : rouge si >=1 session attend, vert sinon. + +### Claude's Discretion +- Protocole i3bar (i3status-rs, i3blocks, ou script custom) +- Implementation des notifications (notify-send vs D-Bus natif) +- Frequence de rafraichissement du widget i3bar +- Stockage du timer focus (en memoire dans le daemon, persiste ou non) + +### Deferred Ideas (OUT OF SCOPE) +None + + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|------------------| +| NOTIF-01 | vmux notifie (dunst) quand une session passe de "travaille" a "attend input" | Transition detection dans processHookEvent + UpdateFromHook. notify-send disponible. | +| NOTIF-02 | vmux supporte un mode focus qui supprime temporairement les notifications | Timer en memoire dans Daemon, nouvelle action "focus" sur le socket Unix. | +| I3-03 | vmux fournit un widget i3bar affichant le statut des sessions en temps reel | Script/binaire standalone parlant i3bar protocol JSON, query daemon via socket. | + + +## Standard Stack + +### Core +| Library | Version | Purpose | Why Standard | +|---------|---------|---------|--------------| +| notify-send (CLI) | 0.8.8 | Notifications desktop | Deja installe, zero dependance Go, compatible dunst. Suffisant pour des notifications simples (titre + corps + urgence). | +| i3bar protocol v1 | - | Widget status bar | Protocole natif i3. JSON streaming. Pas besoin de lib, format trivial. | + +### Supporting +| Library | Version | Purpose | When to Use | +|---------|---------|---------|-------------| +| os/exec (stdlib) | - | Appeler notify-send | Pour envoyer les notifications desktop | +| encoding/json (stdlib) | - | i3bar JSON output | Formater la sortie i3bar protocol | +| path/filepath (stdlib) | - | Extraire nom court du cwd | Dernier segment du chemin | +| strconv (stdlib) | - | Parser duree focus CLI | Convertir argument minutes | + +### Alternatives Considered +| Instead of | Could Use | Tradeoff | +|------------|-----------|----------| +| notify-send (os/exec) | esiqveland/notify v0.13.3 (D-Bus natif) | D-Bus natif evite la dependance au binaire notify-send, mais ajoute 2 deps Go (godbus, notify). notify-send est deja installe et suffisant pour des notifications sans actions. | +| Script i3bar custom | i3blocks | i3blocks n'est pas installe. Le script custom est plus simple et n'ajoute pas de dependance. | +| Script i3bar custom | i3status-rs | Over-engineering. Un simple script qui query le socket suffit. | + +**Decision notifications:** Utiliser `notify-send` via `os/exec`. Raisons : zero dependance supplementaire, deja installe (v0.8.8 sur la machine), dunst le gere nativement, les notifications vmux sont simples (pas d'actions, pas de callbacks). Si un jour on a besoin de callbacks ou de remplacer des notifications, migrer vers esiqveland/notify sera simple. + +**Decision i3bar:** Script standalone integre dans le binaire vmux (`vmux i3bar` sous-commande). Il parle directement le protocole i3bar JSON sur stdout. Remplace i3status dans la config i3. + +## Architecture Patterns + +### Recommended Project Structure (new files) +``` +notify.go # Notifier interface + notify-send implementation +notify_test.go # Tests notification (mock exec) +focus.go # FocusTimer struct +focus_test.go # Tests focus timer +i3bar.go # i3bar JSON output, FormatI3Bar +i3bar_test.go # Tests i3bar formatting +``` + +### Pattern 1: Notifier Interface +**What:** Interface pour decouple l'envoi de notifications du mecanisme. +**When to use:** Toujours, pour la testabilite. +**Example:** +```go +// Notifier sends desktop notifications. +type Notifier interface { + Notify(title, body string) error +} + +// ExecNotifier sends notifications via notify-send. +type ExecNotifier struct{} + +func (n *ExecNotifier) Notify(title, body string) error { + return exec.Command("notify-send", "--urgency=critical", title, body).Run() +} + +// NullNotifier drops all notifications (for tests or focus mode). +type NullNotifier struct{} + +func (n *NullNotifier) Notify(title, body string) error { return nil } +``` + +### Pattern 2: Focus Timer en memoire +**What:** Timestamp d'expiration dans le Daemon. Si `time.Now() < expiration`, pas de notification. +**When to use:** Pour `vmux focus 30`. +**Example:** +```go +type FocusTimer struct { + mu sync.Mutex + expires time.Time +} + +func (f *FocusTimer) Set(duration time.Duration) { + f.mu.Lock() + defer f.mu.Unlock() + f.expires = time.Now().Add(duration) +} + +func (f *FocusTimer) IsActive() bool { + f.mu.Lock() + defer f.mu.Unlock() + return time.Now().Before(f.expires) +} + +func (f *FocusTimer) Remaining() time.Duration { + f.mu.Lock() + defer f.mu.Unlock() + r := time.Until(f.expires) + if r < 0 { + return 0 + } + return r +} +``` + +### Pattern 3: Notification dans processHookEvent +**What:** Declencher la notification apres detection de transition Working -> Needs Input. +**When to use:** Dans processHookEvent, apres UpdateFromHook. +**Example:** +```go +func (d *Daemon) processHookEvent(event HookEvent) { + // ... existing logic ... + + prevState := d.registry.GetPrevState(event.SessionID) + d.registry.UpdateFromHook(event.SessionID, state, waitType, event.Cwd) + + // Notification: only Working -> Needs Input (D-01) + if state == "Needs Input" && prevState == "Working" { + if !d.focus.IsActive() { + name := d.sessionShortName(event.SessionID) + d.notifier.Notify("vmux: "+name, "Session needs input ("+waitType+")") + } + } +} +``` + +### Pattern 4: i3bar Protocol Output +**What:** Sous-commande `vmux i3bar` qui parle le protocole i3bar sur stdout. +**When to use:** Comme status_command dans la config i3. +**Example:** +```go +// i3bar protocol: header then infinite JSON array +func runI3Bar(sockPath string) { + fmt.Println(`{"version":1}`) + fmt.Println("[") + + first := true + for { + client := NewClient(sockPath) + resp, err := client.Send(Request{Action: "list"}) + + if !first { + fmt.Print(",") + } + first = false + + blocks := formatI3BarBlocks(resp.Sessions) + data, _ := json.Marshal(blocks) + fmt.Println(string(data)) + + time.Sleep(2 * time.Second) + } +} +``` + +### Pattern 5: i3bar Block Format +**What:** Un seul bloc i3bar avec le format compact D-06/D-07. +**Example:** +```go +type I3BarBlock struct { + FullText string `json:"full_text"` + ShortText string `json:"short_text,omitempty"` + Color string `json:"color"` + Name string `json:"name"` + Urgent bool `json:"urgent,omitempty"` +} + +func formatI3BarBlocks(sessions []SessionInfo) []I3BarBlock { + hasWaiting := false + parts := make([]string, 0, len(sessions)) + + for _, s := range sessions { + name := shortName(s) // label ou dernier segment cwd + switch s.State { + case "Needs Input": + parts = append(parts, name+"[!]") + hasWaiting = true + case "Working": + parts = append(parts, name+"[W]") + case "Idle": + parts = append(parts, name+"[I]") + } + } + + text := "vmux: " + strings.Join(parts, " ") + if !hasWaiting && len(sessions) > 0 { + text = fmt.Sprintf("vmux: all working (%d)", len(sessions)) + } + + color := "#00ff00" // vert + if hasWaiting { + color = "#ff0000" // rouge + } + + return []I3BarBlock{{FullText: text, Color: color, Name: "vmux"}} +} +``` + +### Anti-Patterns to Avoid +- **Ne pas persister le focus timer:** En memoire suffit. Si le daemon redemarre, le focus se reset. Simple et correct. +- **Ne pas wrapper i3status:** La machine utilise i3status comme status_command. Wrapper sa sortie JSON pour injecter le bloc vmux est fragile. Mieux : remplacer i3status par `vmux i3bar` qui inclut optionnellement la sortie i3status via pipe. +- **Ne pas utiliser i3blocks:** Pas installe, ajoute une dependance. + +## Don't Hand-Roll + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| Desktop notifications | Client D-Bus custom | `notify-send` via os/exec | Gere les edge cases (bus absent, daemon down). notify-send echoue silencieusement. | +| i3bar protocol | Parser i3bar protocol | JSON encode + stdout | Le protocole est trivial (header + array JSON). Pas besoin de lib. | +| Timer thread-safe | sync.Mutex manual | `sync.Mutex` dans FocusTimer struct | Le pattern est simple, pas besoin de lib externe. | + +## Common Pitfalls + +### Pitfall 1: PrevState non accessible dans processHookEvent +**What goes wrong:** processHookEvent ne connait pas l'ancien etat avant UpdateFromHook. Or on a besoin de savoir si c'etait "Working" pour D-01. +**Why it happens:** UpdateFromHook ecrase PrevState avant qu'on puisse le lire. +**How to avoid:** Lire PrevState AVANT d'appeler UpdateFromHook, ou ajouter une methode qui retourne l'ancien etat. La methode `GetPrevState(sessionID) string` est la plus propre. +**Warning signs:** Notifications envoyees pour toutes les transitions vers "Needs Input", pas seulement depuis "Working". + +### Pitfall 2: i3bar stdout buffering +**What goes wrong:** Les mises a jour i3bar n'apparaissent pas en temps reel. +**Why it happens:** Go bufferise stdout par defaut quand ce n'est pas un terminal. +**How to avoid:** Utiliser `os.Stdout.Write()` directement ou `bufio.Writer` avec Flush() apres chaque ligne. +**Warning signs:** Le widget se met a jour par "bursts" au lieu de chaque 2s. + +### Pitfall 3: notify-send bloque si D-Bus est down +**What goes wrong:** `exec.Command("notify-send", ...).Run()` bloque ou retourne une erreur. +**Why it happens:** Le bus D-Bus de session peut etre indisponible temporairement. +**How to avoid:** Utiliser `exec.CommandContext` avec un timeout de 5s. Logger l'erreur et continuer. +**Warning signs:** Le daemon se bloque periodiquement. + +### Pitfall 4: i3bar remplace i3status +**What goes wrong:** En remplacement de i3status par `vmux i3bar`, on perd les infos systeme (heure, batterie, etc.). +**Why it happens:** i3status fournit des blocs systeme que vmux ne connait pas. +**How to avoid:** Deux options : (a) `vmux i3bar` n'affiche que le bloc vmux, on wrap i3status en amont pour combiner. (b) On accepte de ne voir que le bloc vmux. Recommandation : option (a), en executant i3status en subprocess et en injectant le bloc vmux dans sa sortie JSON. +**Warning signs:** L'heure et les infos systeme disparaissent de la barre. + +### Pitfall 5: Race condition sur PrevState +**What goes wrong:** Un poll et un hook modifient PrevState en parallele. +**Why it happens:** scanOnce et processHookEvent tournent dans des goroutines distinctes. +**How to avoid:** Le mutex dans SessionRegistry protege deja les acces. S'assurer que la lecture de PrevState est faite sous le meme verrou que l'ecriture. +**Warning signs:** Notifications fantomes ou manquees. + +## Code Examples + +### Notification avec timeout +```go +func (n *ExecNotifier) Notify(title, body string) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + return exec.CommandContext(ctx, "notify-send", + "--urgency=critical", + "--app-name=vmux", + title, body, + ).Run() +} +``` + +### Short name extraction +```go +func shortName(s SessionInfo) string { + if s.Label != "" { + return s.Label + } + return filepath.Base(s.Cwd) +} +``` + +### i3bar avec i3status wrapping +```go +func runI3Bar(sockPath string, i3statusCmd string) { + // Start i3status as subprocess + cmd := exec.Command(i3statusCmd) + stdout, _ := cmd.StdoutPipe() + cmd.Start() + + scanner := bufio.NewScanner(stdout) + + // Read and forward header + scanner.Scan() + header := scanner.Text() // {"version":1} + fmt.Println(header) + + // Read opening bracket + scanner.Scan() + fmt.Println(scanner.Text()) // [ + + first := true + for scanner.Scan() { + line := scanner.Text() + line = strings.TrimPrefix(line, ",") + + var blocks []I3BarBlock + json.Unmarshal([]byte(line), &blocks) + + // Prepend vmux block + vmuxBlock := getVmuxBlock(sockPath) + blocks = append([]I3BarBlock{vmuxBlock}, blocks...) + + data, _ := json.Marshal(blocks) + if !first { + fmt.Print(",") + } + first = false + fmt.Println(string(data)) + } +} +``` + +### Focus handler dans daemon +```go +// Dans handleConnection, ajouter: +case "focus": + var args FocusArgs + if err := json.Unmarshal(req.Args, &args); err != nil { + writeResponse(conn, Response{Error: "invalid focus args: " + err.Error()}) + return + } + d.focus.Set(time.Duration(args.Minutes) * time.Minute) + writeResponse(conn, Response{OK: true}) +``` + +## State of the Art + +| Old Approach | Current Approach | When Changed | Impact | +|--------------|------------------|--------------|--------| +| libnotify (C) | notify-send CLI / D-Bus Go libs | Stable depuis 10+ ans | notify-send est le standard de facto | +| i3status (C) | i3status / i3status-rs / i3blocks | i3blocks stable, i3status-rs recent | i3status est deja en place sur cette machine | +| i3bar protocol v1 | Inchange | Stable depuis i3 4.x | Format JSON simple, pas d'evolution prevue | + +## Open Questions + +1. **i3status wrapping vs vmux-only bar** + - What we know: La machine utilise i3status comme status_command. Remplacer par vmux i3bar perd les infos systeme. + - What's unclear: Est-ce que l'utilisateur veut garder les infos i3status (heure, etc.) ? + - Recommendation: Wrapper i3status par defaut. `vmux i3bar` execute i3status en subprocess, parse sa sortie JSON, et injecte le bloc vmux en tete. Si i3status n'est pas disponible, affiche uniquement le bloc vmux. + +2. **Frequence de rafraichissement i3bar** + - What we know: 2s est un bon compromis. Les hooks arrivent en temps reel, mais le widget query le daemon periodiquement. + - Recommendation: 2 secondes. Pas de signal push du daemon vers le script i3bar (over-engineering pour v1). + +## Environment Availability + +| Dependency | Required By | Available | Version | Fallback | +|------------|------------|-----------|---------|----------| +| notify-send | NOTIF-01 | oui | 0.8.8 | esiqveland/notify (D-Bus Go natif) | +| dunst | NOTIF-01 | oui | 1.13.1 | Tout daemon freedesktop-notifications | +| i3bar | I3-03 | oui | i3 4.24 | - | +| i3status | I3-03 (wrapping) | oui | 2.15 | vmux-only bar (pas de blocs systeme) | +| i3blocks | - | non | - | Non necessaire, script custom | + +**Missing dependencies with no fallback:** Aucun. +**Missing dependencies with fallback:** Aucun. + +## Validation Architecture + +### Test Framework +| Property | Value | +|----------|-------| +| Framework | Go testing (stdlib) | +| Config file | Aucun (conventions Go) | +| Quick run command | `go test ./... -count=1 -short` | +| Full suite command | `go test ./... -count=1 -race` | + +### Phase Requirements -> Test Map +| Req ID | Behavior | Test Type | Automated Command | File Exists? | +|--------|----------|-----------|-------------------|-------------| +| NOTIF-01 | Notification sur transition Working -> Needs Input | unit | `go test -run TestNotify -count=1` | Non, Wave 0 | +| NOTIF-01 | Pas de notification pour Idle -> Needs Input | unit | `go test -run TestNotifyOnlyFromWorking -count=1` | Non, Wave 0 | +| NOTIF-02 | Focus timer bloque les notifications | unit | `go test -run TestFocusTimer -count=1` | Non, Wave 0 | +| NOTIF-02 | Focus timer expire apres la duree | unit | `go test -run TestFocusExpires -count=1` | Non, Wave 0 | +| I3-03 | Format i3bar bloc compact | unit | `go test -run TestFormatI3Bar -count=1` | Non, Wave 0 | +| I3-03 | Couleur rouge si session attend | unit | `go test -run TestI3BarColor -count=1` | Non, Wave 0 | +| I3-03 | "all working" quand aucune attente | unit | `go test -run TestI3BarAllWorking -count=1` | Non, Wave 0 | + +### Sampling Rate +- **Per task commit:** `go test ./... -count=1 -short` +- **Per wave merge:** `go test ./... -count=1 -race` +- **Phase gate:** Full suite green before `/gsd:verify-work` + +### Wave 0 Gaps +- [ ] `notify_test.go` - tests Notifier interface, mock exec, focus integration +- [ ] `focus_test.go` - tests FocusTimer Set/IsActive/Remaining/expiration +- [ ] `i3bar_test.go` - tests formatI3BarBlocks, shortName, couleurs, "all working" + +## Sources + +### Primary (HIGH confidence) +- [i3bar protocol](https://i3wm.org/docs/i3bar-protocol.html) - Protocol v1 specification, block properties, JSON format +- Machine locale - notify-send 0.8.8, dunst 1.13.1, i3 4.24, i3status 2.15 verifies +- Code source vmux existant - daemon.go, hook.go, protocol.go, main.go + +### Secondary (MEDIUM confidence) +- [esiqveland/notify](https://github.com/esiqveland/notify) - v0.13.3, API Go D-Bus, BSD-3-Clause +- [esiqveland/notify API](https://pkg.go.dev/github.com/esiqveland/notify) - Notification struct, Notifier interface, urgency levels +- [i3blocks](https://vivien.github.io/i3blocks/) - Alternative status_command (non retenue) +- [i3status wrapper gist](https://gist.github.com/reinefjord/cf4b42c9ba8f87a988bc273474d6ef38) - Pattern pour injecter des blocs dans i3status JSON + +### Tertiary (LOW confidence) +- Aucun + +## Project Constraints (from CLAUDE.md) + +- NixOS avec i3 (nix-shell pour les deps) +- Go stdlib preferred, coherence avec piaire +- TDD Chicago School +- Code simple, expressif, bien concu +- Commentaires uniquement pour le "pourquoi" +- Ne jamais modifier les .env +- Nouveaux parametres obligatoires (optionnels seulement si retro-compatibilite) + +## Metadata + +**Confidence breakdown:** +- Standard stack: HIGH - notify-send et i3bar protocol sont stables depuis des annees, verifies sur la machine +- Architecture: HIGH - Les patterns sont derives du code existant (Notifier interface, daemon handlers) +- Pitfalls: HIGH - Identifies par analyse du code existant et connaissance du protocole i3bar + +**Research date:** 2026-03-24 +**Valid until:** 2026-04-24 (stack tres stable)