feat: implémentation complete v0.1.0 — daemon de sync clipboard bidirectionnel
CI / Lint (push) Failing after 14m46s
CI / Test (push) Failing after 4m23s
CI / Build (darwin/amd64) (push) Skipped
CI / Build (linux/amd64) (push) Skipped
CI / Build (windows/amd64) (push) Skipped
CI / Build (darwin/arm64) (push) Skipped
CI / Build (linux/arm64) (push) Skipped
CI / Build (windows/arm64) (push) Skipped
CI / Release (push) Skipped

This commit is contained in:
2026-08-08 15:47:22 -04:00
parent 736c9024f3
commit 5655573fc8
25 changed files with 2050 additions and 403 deletions
+109 -51
View File
@@ -1,72 +1,130 @@
# clip-sync — Architecture
# ARCHITECTURE.md — clip-sync
## Principe
## Vue d'ensemble
Un daemon Go (~150 lignes, zéro dépendance hors stdlib) qui synchronise le presse-papier entre machines sur un LAN. Chaque machine tourne le même binaire, écoute sur `:9137`, et pousse les nouveaux clips vers toutes les autres.
clip-sync est un daemon de synchronisation bidirectionnelle de presse-papiers
en mesh pair-à-pair sur réseau local. Chaque nœud est à la fois client et serveur.
## Flux de données
```
┌──────────────┐ POST /clip ┌──────────────┐
│ deepin │◄──────────────►│ vivobook │
│ :9137 │ {text,ts} │ :9137 │
│ │ │ │
│ poll 500ms │ │ poll 500ms │
│ xclip -o │ │ Get-Clip │
└──────────────┘ └──────────────┘
┌────────────────────────────────────┐
│ clip-sync (nœud) │
│ │
Presse-papiers ──►│ Poller (500ms) │
(Ctrl+C) │ │ lecture (OS clipboard) │
│ ▼ │
│ Comparaison (vs dernier texte) │
│ │ si nouveau texte │
│ ▼ │
│ Filtre anti-boucle │
│ │ MarkWritten(hash, ts) │
│ ▼ │
│ Broadcaster │
│ │ POST /clip → chaque pair │
│ │ goroutine par pair │
└────┼───────────────────────────────┘
│
▼
┌───────────────────────┐
│ Pair distant :9137 │
│ POST /clip {json} │
│ ↓ │
│ Filtre anti-boucle │
│ ↓ (si accepté) │
│ Write clipboard │
│ ↓ (Ctrl+V possible)│
└───────────────────────┘
```
## Boucles (tout dans `main.go`)
### Boucle 1 — Watch local
Toutes les 500ms, lit le clipboard local (`xclip -o` / `powershell Get-Clipboard`), le hashe (SHA-256). Si le hash diffère du dernier connu → broadcast HTTP POST à tous les pairs.
### Boucle 2 — Receive
Serveur HTTP `POST /clip`. Reçoit `{text, ts, host}`. Si le host est soi-même ou si le timestamp est ≤ au dernier reçu de ce host → ignore. Sinon, écrit dans le clipboard local ET met à jour `lastHash` pour que la boucle 1 ne rebroadcast pas.
### Anti-boucle d'écho
Après un `clipboard.Write()` déclenché par un clip reçu, `lastHash` est mis à jour avec le hash du texte reçu. Le poller local le voit comme "déjà connu" → pas de rebroadcast.
## Composants
| Fichier | Rôle | Lignes |
|---------|------|--------|
| `main.go` | Serveur HTTP + watch loop + dispatch | ~120 |
| `peers.toml` | Configuration des pairs | ~10 |
| `install.sh` | Installation one-liner cross-OS | ~40 |
### 1. Clipboard (`internal/clipboard/`)
## Plateformes
Interface unifiée `Read() / Write()` avec implémentations par plateforme :
| OS | Clipboard read | Clipboard write |
|----|---------------|-----------------|
| Linux (X11) | `xclip -o -selection clipboard` | `xclip -selection clipboard` |
| Linux (Wayland) | `wl-paste` | `wl-copy` |
| macOS | `pbpaste` | `pbcopy` |
| Windows | `powershell -Command Get-Clipboard` | `powershell -Command Set-Clipboard -Value ...` |
- **Linux** : auto-détection X11/Wayland via `$XDG_SESSION_TYPE`
- X11 → `xclip -selection clipboard -o` / `xclip -selection clipboard`
- Wayland → `wl-paste` / `wl-copy`
- **Windows** : Win32 API via `syscall` (user32.dll, kernel32.dll), zéro CGo
- **macOS** : `pbpaste` / `pbcopy`
Détection automatique au démarrage via `runtime.GOOS` + test de présence des binaires.
### 2. Configuration (`internal/config/`)
## Ce qui est volontairement omis
- Fichier TOML : `~/.config/clip-sync/peers.toml`
- Parsing : `github.com/BurntSushi/toml` (seule dépendance externe — TOML absent de stdlib)
- Override par variables d'environnement : `CLIP_SYNC_PORT`, `CLIP_SYNC_POLL_MS`
- **Chiffrement** — LAN de confiance. Si besoin → TLS entre pairs (v2).
- **Découverte automatique (mDNS)** — config statique = 5 lignes de TOML. Discovery = 200 lignes. v2.
- **Historique / ring buffer** — ce n'est pas un gestionnaire de presse-papier, c'est du live sync.
- **UI / tray icon** — daemon headless, `systemd --user` ou `nssm` pour Windows.
- **Auth token** — HTTP en clair sur LAN. Port 9137 non exposé à l'extérieur.
### 3. Anti-boucle (`internal/dedup/`)
Deux garde-fous empêchent les boucles de synchronisation :
| Garde-fou | Mécanisme | Scénario bloqué |
|-----------|-----------|-----------------|
| **Hash** | Cache LRU (20 derniers) des SHA-256 locaux | Notre texte nous revient via un pair |
| **Origine** | Comparaison `origin == hostname` | Rebond via 3e pair |
| **Timestamp** | `ts <= lastWriteTs` | Contournement multi-pair |
Payload JSON :
```json
{
"text": "contenu du presse-papiers",
"ts": 1690000000000000000,
"origin": "nom-de-machine"
}
```
### 4. Communication pair-à-pair (`internal/peer/`, `internal/server/`)
- Protocole : HTTP POST `/clip` (JSON), réponse `204 No Content`
- Timeout connexion : 3s (dial), 5s (request)
- Chaque broadcast aux pairs est lancé dans sa propre goroutine (non-bloquant)
- Échec d'un pair = log + continue (pas de blocage des autres)
### 5. Daemon (`internal/daemon/`)
Boucle principale :
1. Démarre le serveur HTTP dans une goroutine
2. Ticker 500ms : lit le presse-papiers, compare, broadcast si changé
3. Signal SIGINT/SIGTERM → arrêt gracieux
## Dépendances
Zéro. `net/http`, `crypto/sha256`, `encoding/json`, `os/exec`, `runtime`. Stdlib Go pure.
| Dépendance | Version | Justification |
|-----------|---------|---------------|
| `github.com/BurntSushi/toml` | v1.3.2 | Parsing TOML (stdlib Go n'inclut pas TOML) |
## Build
**Total : 1 dépendance externe.** Tout le reste est stdlib Go.
```bash
# Linux
GOOS=linux GOARCH=amd64 go build -o clip-sync .
## Décisions architecturales
# Windows
GOOS=windows GOARCH=amd64 go build -o clip-sync.exe .
### Pourquoi HTTP et pas TCP raw / gRPC ?
# macOS
GOOS=darwin GOARCH=amd64 go build -o clip-sync .
```
- Débogable avec `curl`
- `net/http` est dans la stdlib, zéro dépendance
- Le surcoût HTTP est négligeable pour du texte de presse-papiers
- Facile à étendre (TLS, auth headers) dans les versions futures
Un binaire statique par OS. Pas de Docker, pas de runtime.
### Pourquoi TOML et pas YAML/JSON ?
- TOML est le format standard de configuration en Go
- Plus lisible que JSON pour des humains
- Pas de problème de whitespace-significant comme YAML
### Pourquoi pas mDNS en v1.0 ?
- Philosophie du projet : configuration explicite, zéro magie
- mDNS ajoute une complexité significative (multicast, découverte, timeouts)
- Prévu pour la v1.0 dans la roadmap
### Pourquoi du polling (500ms) et pas des events OS ?
- Les APIs de notification de presse-papiers sont non-portables (Win32 `AddClipboardFormatListener`, X11 `XFixes`, etc.)
- Le polling 500ms est un compromis acceptable entre réactivité et CPU
- Un seul `Read()` par tick, coût négligeable
### Pourquoi un cache LRU de 20 hash et pas plus ?
- 20 entrées couvrent ~10 secondes de copies rapides à 500ms/poll
- Au-delà, le risque de collision est négligeable (SHA-256)
- Garde la mémoire constante et prévisible