Files
vmux/.planning/phases/04-notifications-et-i3bar/04-RESEARCH.md
2026-03-23 20:48:03 +01:00

19 KiB

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>

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 </user_constraints>

<phase_requirements>

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.
</phase_requirements>

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

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:

// 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:

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:

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:

// 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:

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

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

func shortName(s SessionInfo) string {
    if s.Label != "" {
        return s.Label
    }
    return filepath.Base(s.Cwd)
}

i3bar avec i3status wrapping

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

// 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 - 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)

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)