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
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:
@@ -0,0 +1,98 @@
|
||||
// Package config parses the clip-sync TOML configuration file and applies
|
||||
// environment variable overrides.
|
||||
//
|
||||
// Config file location: ~/.config/clip-sync/peers.toml
|
||||
// Environment overrides:
|
||||
// CLIP_SYNC_PORT — override daemon.port
|
||||
// CLIP_SYNC_POLL_MS — override daemon.poll_interval_ms
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
// Config represents the full clip-sync configuration.
|
||||
type Config struct {
|
||||
Daemon DaemonConfig `toml:"daemon"`
|
||||
Peers []PeerConfig `toml:"peers"`
|
||||
}
|
||||
|
||||
// DaemonConfig holds daemon-level settings.
|
||||
type DaemonConfig struct {
|
||||
Port int `toml:"port"`
|
||||
PollIntervalMs int `toml:"poll_interval_ms"`
|
||||
}
|
||||
|
||||
// PeerConfig represents one remote clip-sync peer.
|
||||
type PeerConfig struct {
|
||||
Name string `toml:"name"`
|
||||
Addr string `toml:"addr"`
|
||||
}
|
||||
|
||||
// Default values.
|
||||
const (
|
||||
DefaultPort = 9137
|
||||
DefaultPollIntervalMs = 500
|
||||
ConfigDir = ".config/clip-sync"
|
||||
ConfigFile = "peers.toml"
|
||||
)
|
||||
|
||||
// Load reads the config file from ~/.config/clip-sync/peers.toml and applies
|
||||
// environment variable overrides.
|
||||
func Load() (*Config, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("config: cannot find home directory: %w", err)
|
||||
}
|
||||
|
||||
cfgPath := filepath.Join(home, ConfigDir, ConfigFile)
|
||||
|
||||
cfg := &Config{
|
||||
Daemon: DaemonConfig{
|
||||
Port: DefaultPort,
|
||||
PollIntervalMs: DefaultPollIntervalMs,
|
||||
},
|
||||
}
|
||||
|
||||
if _, err := toml.DecodeFile(cfgPath, cfg); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("config: cannot parse %s: %w", cfgPath, err)
|
||||
}
|
||||
// File doesn't exist — use defaults (no peers configured).
|
||||
}
|
||||
|
||||
// Environment variable overrides.
|
||||
if v := os.Getenv("CLIP_SYNC_PORT"); v != "" {
|
||||
port, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("config: invalid CLIP_SYNC_PORT: %w", err)
|
||||
}
|
||||
cfg.Daemon.Port = port
|
||||
}
|
||||
|
||||
if v := os.Getenv("CLIP_SYNC_POLL_MS"); v != "" {
|
||||
ms, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("config: invalid CLIP_SYNC_POLL_MS: %w", err)
|
||||
}
|
||||
cfg.Daemon.PollIntervalMs = ms
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// PollInterval returns the poll interval as a time.Duration.
|
||||
func (c *Config) PollInterval() time.Duration {
|
||||
return time.Duration(c.Daemon.PollIntervalMs) * time.Millisecond
|
||||
}
|
||||
|
||||
// ListenAddr returns the HTTP listen address for this daemon.
|
||||
func (c *Config) ListenAddr() string {
|
||||
return fmt.Sprintf(":%d", c.Daemon.Port)
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadDefaults(t *testing.T) {
|
||||
// Point HOME/USERPROFILE to a temp dir with no config file.
|
||||
tmp := t.TempDir()
|
||||
t.Setenv("HOME", tmp)
|
||||
t.Setenv("USERPROFILE", tmp) // Windows compatibility
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Daemon.Port != DefaultPort {
|
||||
t.Errorf("default port = %d, want %d", cfg.Daemon.Port, DefaultPort)
|
||||
}
|
||||
if cfg.Daemon.PollIntervalMs != DefaultPollIntervalMs {
|
||||
t.Errorf("default poll = %d, want %d", cfg.Daemon.PollIntervalMs, DefaultPollIntervalMs)
|
||||
}
|
||||
if len(cfg.Peers) != 0 {
|
||||
t.Errorf("default peers = %d, want 0", len(cfg.Peers))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFromFile(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
cfgDir := filepath.Join(tmp, ConfigDir)
|
||||
os.MkdirAll(cfgDir, 0755)
|
||||
|
||||
content := `
|
||||
[daemon]
|
||||
port = 9999
|
||||
poll_interval_ms = 250
|
||||
|
||||
[[peers]]
|
||||
name = "bureau"
|
||||
addr = "192.168.1.10:9137"
|
||||
|
||||
[[peers]]
|
||||
name = "portable"
|
||||
addr = "192.168.1.20:9137"
|
||||
`
|
||||
os.WriteFile(filepath.Join(cfgDir, ConfigFile), []byte(content), 0644)
|
||||
t.Setenv("HOME", tmp)
|
||||
t.Setenv("USERPROFILE", tmp) // Windows compatibility
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Daemon.Port != 9999 {
|
||||
t.Errorf("port = %d, want 9999", cfg.Daemon.Port)
|
||||
}
|
||||
if cfg.Daemon.PollIntervalMs != 250 {
|
||||
t.Errorf("poll = %d, want 250", cfg.Daemon.PollIntervalMs)
|
||||
}
|
||||
if len(cfg.Peers) != 2 {
|
||||
t.Fatalf("peers = %d, want 2", len(cfg.Peers))
|
||||
}
|
||||
if cfg.Peers[0].Name != "bureau" {
|
||||
t.Errorf("peer[0].Name = %q, want \"bureau\"", cfg.Peers[0].Name)
|
||||
}
|
||||
if cfg.Peers[1].Addr != "192.168.1.20:9137" {
|
||||
t.Errorf("peer[1].Addr = %q", cfg.Peers[1].Addr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvOverride(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
t.Setenv("HOME", tmp)
|
||||
t.Setenv("USERPROFILE", tmp) // Windows compatibility
|
||||
t.Setenv("CLIP_SYNC_PORT", "8080")
|
||||
t.Setenv("CLIP_SYNC_POLL_MS", "100")
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Daemon.Port != 8080 {
|
||||
t.Errorf("port = %d, want 8080", cfg.Daemon.Port)
|
||||
}
|
||||
if cfg.Daemon.PollIntervalMs != 100 {
|
||||
t.Errorf("poll = %d, want 100", cfg.Daemon.PollIntervalMs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvOverrideInvalidPort(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
t.Setenv("HOME", tmp)
|
||||
t.Setenv("USERPROFILE", tmp) // Windows compatibility
|
||||
t.Setenv("CLIP_SYNC_PORT", "not-a-number")
|
||||
|
||||
_, err := Load()
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid CLIP_SYNC_PORT, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPollInterval(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Daemon: DaemonConfig{PollIntervalMs: 500},
|
||||
}
|
||||
if d := cfg.PollInterval(); d.Milliseconds() != 500 {
|
||||
t.Errorf("PollInterval = %v, want 500ms", d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenAddr(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Daemon: DaemonConfig{Port: 9137},
|
||||
}
|
||||
if addr := cfg.ListenAddr(); addr != ":9137" {
|
||||
t.Errorf("ListenAddr = %q, want \":9137\"", addr)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user