// clip-sync — bidirectional LAN clipboard synchronization daemon. // // Usage: // // clip-sync [flags] # run the daemon // clip-sync history # print local clipboard history // clip-sync send-file # send a file to all peers // clip-sync --one-shot [text] # send the clipboard (or text) once, then exit // // Flags: // // --config path config file (default ~/.config/clip-sync/peers.toml) // --version print version and exit // --generate-key generate a shared key and exit // --generate-cert generate a self-signed TLS cert/key and exit package main import ( "context" "flag" "fmt" "log" "os" "os/signal" "strings" "syscall" "time" "git.dracodev.net/Projets/clip-sync/internal/clipboard" "git.dracodev.net/Projets/clip-sync/internal/config" "git.dracodev.net/Projets/clip-sync/internal/daemon" "git.dracodev.net/Projets/clip-sync/internal/history" "git.dracodev.net/Projets/clip-sync/internal/peer" "git.dracodev.net/Projets/clip-sync/internal/security" "git.dracodev.net/Projets/clip-sync/internal/transfer" ) // version is overridden at build time via -ldflags "-X main.version=...". var version = "0.2.0" func main() { log.SetFlags(log.LstdFlags | log.Lshortfile) log.SetPrefix("") fs := flag.NewFlagSet("clip-sync", flag.ExitOnError) configPath := fs.String("config", "", "config file path") showVersion := fs.Bool("version", false, "print version and exit") generateKey := fs.Bool("generate-key", false, "generate a shared key and exit") generateCert := fs.Bool("generate-cert", false, "generate a self-signed TLS certificate and exit") oneShot := fs.Bool("one-shot", false, "send the clipboard once and exit") fs.Usage = func() { fmt.Fprintf(os.Stderr, "Usage:\n clip-sync [flags]\n clip-sync history\n\nFlags:\n") fs.PrintDefaults() } _ = fs.Parse(os.Args[1:]) // Subcommand: history if fs.NArg() > 0 && fs.Arg(0) == "history" { runHistory() return } if *showVersion { fmt.Printf("clip-sync %s\n", version) return } if *generateKey { key, err := security.GenerateKey() if err != nil { log.Fatalf("clip-sync: generate key: %v", err) } fmt.Println(key) return } if *generateCert { runGenerateCert() return } cfg, err := loadConfig(*configPath) if err != nil { log.Fatalf("clip-sync: config: %v", err) } if *oneShot { runOneShot(cfg, fs.Arg(0)) return } // Subcommand: send-file if fs.NArg() > 0 && fs.Arg(0) == "send-file" { path := "" if fs.NArg() > 1 { path = fs.Arg(1) } runSendFile(cfg, path) return } runDaemon(cfg) } func loadConfig(path string) (*config.Config, error) { if path != "" { return config.LoadFrom(path) } return config.Load() } func runDaemon(cfg *config.Config) { d, err := daemon.New(cfg) if err != nil { log.Fatalf("clip-sync: init: %v", err) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) go func() { sig := <-sigCh log.Printf("clip-sync: received %v, shutting down...", sig) cancel() }() if err := d.Run(ctx); err != nil && err != context.Canceled { log.Fatalf("clip-sync: %v", err) } log.Println("clip-sync: stopped") } func runOneShot(cfg *config.Config, textArg string) { origin, err := os.Hostname() if err != nil { log.Fatalf("clip-sync: hostname: %v", err) } b := peer.NewBroadcaster(cfg.Peers, origin, peer.Options{ SharedKey: cfg.Security.SharedKey, TLS: cfg.Security.TLS, CertFile: cfg.Security.CertFile, InsecureSkipVerify: cfg.Security.InsecureSkipVerify, }) text := textArg if text == "" { cb, err := clipboard.New() if err != nil { log.Fatalf("clip-sync: clipboard: %v", err) } text, err = cb.Read() if err != nil { log.Fatalf("clip-sync: clipboard read: %v", err) } } if text == "" { log.Fatal("clip-sync: nothing to send (clipboard is empty)") } b.BroadcastSync(text) fmt.Printf("clip-sync: sent %d bytes to %d peer(s)\n", len(text), b.PeerCount()) } func runSendFile(cfg *config.Config, path string) { if path == "" { log.Fatal("clip-sync: usage: clip-sync send-file ") } origin, err := os.Hostname() if err != nil { log.Fatalf("clip-sync: hostname: %v", err) } f, err := transfer.ReadFile(path, origin) if err != nil { log.Fatalf("clip-sync: read file: %v", err) } b := peer.NewBroadcaster(cfg.Peers, origin, peer.Options{ SharedKey: cfg.Security.SharedKey, TLS: cfg.Security.TLS, CertFile: cfg.Security.CertFile, InsecureSkipVerify: cfg.Security.InsecureSkipVerify, }) b.SendFileSync(*f) fmt.Printf("clip-sync: sent %q (%s) to %d peer(s)\n", f.Name, f.Mime, b.PeerCount()) } func runHistory() { path, err := history.DefaultPath() if err != nil { log.Fatalf("clip-sync: history: %v", err) } entries := history.New(path, 0).List() if len(entries) == 0 { fmt.Println("clip-sync: no history yet") return } for _, e := range entries { ts := time.Unix(0, e.Ts).Format("2006-01-02 15:04:05") text := strings.ReplaceAll(e.Text, "\n", "\\n") if len(text) > 80 { text = text[:80] + "…" } fmt.Printf("%s [%s] %s\n", ts, e.Origin, text) } } func runGenerateCert() { certFile, err := config.DefaultCertFile() if err != nil { log.Fatalf("clip-sync: cert path: %v", err) } keyFile, err := config.DefaultKeyFile() if err != nil { log.Fatalf("clip-sync: key path: %v", err) } host, _ := os.Hostname() if err := security.GenerateSelfSignedCert(certFile, keyFile, []string{host, "localhost"}); err != nil { log.Fatalf("clip-sync: generate cert: %v", err) } fmt.Printf("clip-sync: certificate written to %s and %s\n", certFile, keyFile) fmt.Println("Set [security] tls = true and cert_file/key_file in peers.toml, and share these files with your peers.") }