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
+120
View File
@@ -0,0 +1,120 @@
// Package daemon orchestrates the main clip-sync loop: poll the local clipboard,
// broadcast changes to peers, and receive incoming clips via the HTTP server.
package daemon
import (
"context"
"log"
"os"
"sync"
"time"
"git.dracodev.net/Projets/clip-sync/internal/clipboard"
"git.dracodev.net/Projets/clip-sync/internal/config"
"git.dracodev.net/Projets/clip-sync/internal/dedup"
"git.dracodev.net/Projets/clip-sync/internal/peer"
"git.dracodev.net/Projets/clip-sync/internal/server"
)
// Daemon is the main clip-sync process.
type Daemon struct {
cfg *config.Config
clipboard clipboard.Clipboard
filter *dedup.Filter
server *server.Server
broadcaster *peer.Broadcaster
}
// New creates a Daemon from the given configuration.
func New(cfg *config.Config) (*Daemon, error) {
cb, err := clipboard.New()
if err != nil {
return nil, err
}
return newWithClipboard(cfg, cb)
}
// newWithClipboard creates a Daemon with a pre-existing clipboard (used by tests).
func newWithClipboard(cfg *config.Config, cb clipboard.Clipboard) (*Daemon, error) {
return newWithClipboardOrigin(cfg, cb, "")
}
// newWithClipboardOrigin creates a Daemon with a pre-existing clipboard and explicit origin.
func newWithClipboardOrigin(cfg *config.Config, cb clipboard.Clipboard, origin string) (*Daemon, error) {
if origin == "" {
var err error
origin, err = os.Hostname()
if err != nil {
return nil, err
}
}
filter := dedup.NewFilter(origin)
broadcaster := peer.NewBroadcaster(cfg.Peers, origin)
srv := server.New(cfg.ListenAddr(), cb, filter)
return &Daemon{
cfg: cfg,
clipboard: cb,
filter: filter,
server: srv,
broadcaster: broadcaster,
}, nil
}
// Run starts the daemon loop. It blocks until ctx is cancelled.
func (d *Daemon) Run(ctx context.Context) error {
// Start the HTTP server in a background goroutine.
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
log.Printf("clip-sync: listening on %s", d.cfg.ListenAddr())
if err := d.server.ListenAndServe(); err != nil {
// ListenAndServe returns http.ErrServerClosed on graceful shutdown.
if ctx.Err() == nil {
log.Printf("clip-sync: server error: %v", err)
}
}
}()
// Main clipboard polling loop.
ticker := time.NewTicker(d.cfg.PollInterval())
defer ticker.Stop()
var lastText string
log.Printf("clip-sync: polling clipboard every %v, %d peer(s) configured",
d.cfg.PollInterval(), len(d.cfg.Peers))
for {
select {
case <-ctx.Done():
d.server.Close()
wg.Wait()
return ctx.Err()
case <-ticker.C:
text, err := d.clipboard.Read()
if err != nil {
log.Printf("clip-sync: clipboard read: %v", err)
continue
}
if text == "" || text == lastText {
continue
}
lastText = text
// Record our write so the filter can block echoes.
d.filter.MarkWritten(text)
// Broadcast to all peers.
if len(d.cfg.Peers) > 0 {
d.broadcaster.Broadcast(text)
}
}
}
}