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
+19
View File
@@ -0,0 +1,19 @@
// Package clipboard provides unified clipboard read/write across Linux, Windows, and macOS.
//
// On Linux, it auto-detects X11 (xclip) vs Wayland (wl-clipboard) by checking
// $XDG_SESSION_TYPE and verifying the required binaries at initialization time.
// On Windows, it uses the Win32 clipboard API via syscall (no CGo).
// On macOS, it shells out to pbpaste / pbcopy.
//
// Each platform file defines its own New() constructor using //go:build tags.
package clipboard
// Clipboard is the cross-platform clipboard interface.
type Clipboard interface {
// Read returns the current text content of the clipboard.
// Returns an empty string if the clipboard is empty or contains non-text data.
Read() (string, error)
// Write sets the clipboard text content.
Write(text string) error
}
+45
View File
@@ -0,0 +1,45 @@
//go:build darwin
package clipboard
import (
"fmt"
"os/exec"
"strings"
)
type darwinClipboard struct{}
// New creates a macOS clipboard (pbpaste/pbcopy).
func New() (Clipboard, error) {
return newDarwinClipboard()
}
func newDarwinClipboard() (*darwinClipboard, error) {
// Verify pbpaste and pbcopy are available
if err := checkBinary("pbpaste"); err != nil {
return nil, fmt.Errorf("clip-sync: pbpaste not found: %w", err)
}
if err := checkBinary("pbcopy"); err != nil {
return nil, fmt.Errorf("clip-sync: pbcopy not found: %w", err)
}
return &darwinClipboard{}, nil
}
func (c *darwinClipboard) Read() (string, error) {
cmd := exec.Command("pbpaste")
out, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("clipboard read (pbpaste): %w", err)
}
return strings.TrimRight(string(out), "\n\r"), nil
}
func (c *darwinClipboard) Write(text string) error {
cmd := exec.Command("pbcopy")
cmd.Stdin = strings.NewReader(text)
if err := cmd.Run(); err != nil {
return fmt.Errorf("clipboard write (pbcopy): %w", err)
}
return nil
}
+81
View File
@@ -0,0 +1,81 @@
//go:build linux
package clipboard
import (
"fmt"
"os"
"os/exec"
"strings"
)
type linuxClipboard struct {
copyCmd string // "xclip" or "wl-copy"
pasteCmd string // "xclip" or "wl-paste"
copyArgs []string // e.g. ["-selection", "clipboard"] for xclip
}
// New creates a Linux clipboard (X11 xclip or Wayland wl-clipboard).
func New() (Clipboard, error) {
return newLinuxClipboard()
}
func newLinuxClipboard() (*linuxClipboard, error) {
sessionType := os.Getenv("XDG_SESSION_TYPE")
if strings.EqualFold(sessionType, "wayland") {
if err := checkBinary("wl-copy"); err != nil {
return nil, fmt.Errorf("clip-sync: Wayland detected but wl-copy not found: %w", err)
}
if err := checkBinary("wl-paste"); err != nil {
return nil, fmt.Errorf("clip-sync: Wayland detected but wl-paste not found: %w", err)
}
return &linuxClipboard{
copyCmd: "wl-copy",
pasteCmd: "wl-paste",
copyArgs: nil, // wl-copy reads from stdin by default
}, nil
}
// Default: X11 via xclip
if err := checkBinary("xclip"); err != nil {
return nil, fmt.Errorf("clip-sync: xclip not found (install xclip for X11 or wl-clipboard for Wayland): %w", err)
}
return &linuxClipboard{
copyCmd: "xclip",
pasteCmd: "xclip",
copyArgs: []string{"-selection", "clipboard"},
}, nil
}
func (c *linuxClipboard) Read() (string, error) {
args := []string{"-selection", "clipboard", "-o"}
if c.pasteCmd == "wl-paste" {
args = nil // wl-paste needs no args
}
cmd := exec.Command(c.pasteCmd, args...)
out, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("clipboard read (%s): %w", c.pasteCmd, err)
}
return strings.TrimRight(string(out), "\n\r"), nil
}
func (c *linuxClipboard) Write(text string) error {
cmd := exec.Command(c.copyCmd, c.copyArgs...)
cmd.Stdin = strings.NewReader(text)
if err := cmd.Run(); err != nil {
return fmt.Errorf("clipboard write (%s): %w", c.copyCmd, err)
}
return nil
}
// checkBinary verifies that a command exists in $PATH.
func checkBinary(name string) error {
_, err := exec.LookPath(name)
if err != nil {
return fmt.Errorf("%s: %w", name, err)
}
return nil
}
+106
View File
@@ -0,0 +1,106 @@
//go:build windows
package clipboard
import (
"fmt"
"syscall"
"unsafe"
)
const cfUnicodeText = 13
var (
user32 = syscall.NewLazyDLL("user32.dll")
kernel32 = syscall.NewLazyDLL("kernel32.dll")
procOpenClipboard = user32.NewProc("OpenClipboard")
procCloseClipboard = user32.NewProc("CloseClipboard")
procEmptyClipboard = user32.NewProc("EmptyClipboard")
procGetClipboardData = user32.NewProc("GetClipboardData")
procSetClipboardData = user32.NewProc("SetClipboardData")
procGlobalAlloc = kernel32.NewProc("GlobalAlloc")
procGlobalLock = kernel32.NewProc("GlobalLock")
procGlobalUnlock = kernel32.NewProc("GlobalUnlock")
procGlobalFree = kernel32.NewProc("GlobalFree")
)
type windowsClipboard struct{}
// New creates a Windows clipboard using Win32 API (syscall, no CGo).
func New() (Clipboard, error) {
return newWindowsClipboard()
}
func newWindowsClipboard() (*windowsClipboard, error) {
return &windowsClipboard{}, nil
}
func (c *windowsClipboard) Read() (string, error) {
r1, _, err := procOpenClipboard.Call(0)
if r1 == 0 {
return "", fmt.Errorf("clipboard read: OpenClipboard: %w", err)
}
defer procCloseClipboard.Call()
h, _, err := procGetClipboardData.Call(uintptr(cfUnicodeText))
if h == 0 {
// No text in clipboard — not an error, just empty.
return "", nil
}
p, _, err := procGlobalLock.Call(h)
if p == 0 {
return "", fmt.Errorf("clipboard read: GlobalLock: %w", err)
}
defer procGlobalUnlock.Call(h)
return syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(p))[:]), nil
}
func (c *windowsClipboard) Write(text string) error {
r1, _, err := procOpenClipboard.Call(0)
if r1 == 0 {
return fmt.Errorf("clipboard write: OpenClipboard: %w", err)
}
defer procCloseClipboard.Call()
r1, _, err = procEmptyClipboard.Call()
if r1 == 0 {
return fmt.Errorf("clipboard write: EmptyClipboard: %w", err)
}
utf16, err := syscall.UTF16FromString(text)
if err != nil {
return fmt.Errorf("clipboard write: UTF16FromString: %w", err)
}
// Allocate global memory: size in bytes (2 bytes per UTF-16 unit, including null terminator)
byteSize := uintptr(len(utf16) * 2)
h, _, err := procGlobalAlloc.Call(0, byteSize)
if h == 0 {
return fmt.Errorf("clipboard write: GlobalAlloc: %w", err)
}
p, _, err := procGlobalLock.Call(h)
if p == 0 {
procGlobalFree.Call(h)
return fmt.Errorf("clipboard write: GlobalLock: %w", err)
}
// Copy UTF-16 data into the locked memory.
dst := unsafe.Slice((*uint16)(unsafe.Pointer(p)), len(utf16))
copy(dst, utf16)
procGlobalUnlock.Call(h)
r1, _, err = procSetClipboardData.Call(uintptr(cfUnicodeText), h)
if r1 == 0 {
procGlobalFree.Call(h)
return fmt.Errorf("clipboard write: SetClipboardData: %w", err)
}
// Windows now owns the handle — do not free it.
return nil
}