Skip to content

Commit fc29943

Browse files
authored
chore: prepare clipx for public release (#1)
1 parent 687526b commit fc29943

9 files changed

Lines changed: 173 additions & 30 deletions

File tree

.github/workflows/release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ jobs:
107107
args: release --clean
108108
env:
109109
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
110+
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
110111
GORELEASER_CURRENT_TAG: ${{ steps.version.outputs.tag }}
111112

112113
- name: Mark as pre-release if specified

.goreleaser.yaml

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,20 @@ changelog:
6363
- title: Others
6464
order: 999
6565

66+
brews:
67+
- repository:
68+
owner: gomantics
69+
name: homebrew-tap
70+
token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
71+
directory: Formula
72+
homepage: "https://github.com/gomantics/clipx"
73+
description: "LAN clipboard sync for macOS. Copy on one Mac, paste on another."
74+
license: "MIT"
75+
install: |
76+
bin.install "clipx"
77+
test: |
78+
system "#{bin}/clipx", "version"
79+
6680
release:
6781
github:
6882
owner: gomantics
@@ -76,7 +90,10 @@ release:
7690
### Installation
7791
7892
```bash
79-
# Using Go
93+
# Homebrew
94+
brew install gomantics/tap/clipx
95+
96+
# Go
8097
go install github.com/gomantics/clipx/cmd/clipx@{{ .Tag }}
8198
8299
# Or download the binary from the assets above

CONTRIBUTING.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Contributing to clipx
2+
3+
Thanks for your interest in contributing! Here's how to get started.
4+
5+
## Development
6+
7+
### Prerequisites
8+
9+
- Go 1.25+ (or use [mise](https://mise.jdx.dev/) — the repo includes a `.mise/config.toml`)
10+
- macOS (clipx uses `pbcopy`/`pbpaste`)
11+
12+
### Build & test
13+
14+
```bash
15+
make build # build binary to ./clipx-bin
16+
make test # run tests
17+
make test-race # run tests with race detector
18+
make check # fmt + vet + test
19+
make coverage # generate HTML coverage report
20+
```
21+
22+
### Project structure
23+
24+
```
25+
cmd/clipx/ CLI entry point (commands, LaunchAgent management)
26+
clipx/
27+
clipboard.go Clipboard abstraction (pbcopy/pbpaste)
28+
config.go Config file (~/.config/clipx/config.json)
29+
net.go Network utilities (DNS resolution, ping)
30+
node.go Core daemon (listener, clipboard watcher, peer sync)
31+
protocol.go Wire protocol (encode/decode messages & chunks)
32+
*_test.go Tests
33+
```
34+
35+
### Running locally
36+
37+
```bash
38+
# terminal 1 — start a node
39+
make build && ./clipx-bin
40+
41+
# terminal 2 — pair and test
42+
./clipx-bin pair 127.0.0.1
43+
```
44+
45+
## Submitting changes
46+
47+
1. Fork the repo
48+
2. Create a feature branch (`git checkout -b my-feature`)
49+
3. Make your changes
50+
4. Run `make check` to ensure everything passes
51+
5. Commit with a [conventional commit](https://www.conventionalcommits.org/) message
52+
6. Open a pull request
53+
54+
## Commit messages
55+
56+
We use [conventional commits](https://www.conventionalcommits.org/):
57+
58+
- `feat:` new features
59+
- `fix:` bug fixes
60+
- `perf:` performance improvements
61+
- `docs:` documentation changes
62+
- `test:` test changes
63+
- `chore:` maintenance tasks
64+
65+
## Reporting issues
66+
67+
Please open a [GitHub issue](https://github.com/gomantics/clipx/issues) with:
68+
69+
- Your macOS version
70+
- `clipx version` output
71+
- Steps to reproduce
72+
- Relevant logs from `~/Library/Logs/clipx.log`
73+
74+
## License
75+
76+
By contributing, you agree that your contributions will be licensed under the MIT License.

README.md

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# clipx
22

3+
[![CI](https://github.com/gomantics/clipx/actions/workflows/ci.yml/badge.svg)](https://github.com/gomantics/clipx/actions/workflows/ci.yml)
4+
[![Go Report Card](https://goreportcard.com/badge/github.com/gomantics/clipx)](https://goreportcard.com/report/github.com/gomantics/clipx)
5+
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6+
[![Release](https://img.shields.io/github/v/release/gomantics/clipx)](https://github.com/gomantics/clipx/releases/latest)
7+
38
LAN clipboard sync for macOS. Copy on one Mac, paste on another. Instantly.
49

510
No cloud. No account. No Apple ID. No flaky Universal Clipboard.
@@ -19,25 +24,37 @@ Each Mac runs `clipx`. When you copy something, it sends the clipboard content d
1924
- **UDP unicast** — reliable, fast, no firewall issues with multicast
2025
- **Explicit pairing**`clipx pair <ip>`, no flaky auto-discovery
2126
- **SHA-256 dedup** — prevents infinite ping-pong loops between nodes
22-
- **10MB max** — large content is automatically chunked into 16KB UDP packets
27+
- **10 MB max** — large content is automatically chunked into ~1300-byte UDP packets
2328

24-
## Setup
29+
## Install
2530

26-
### 1. Install on both Macs
31+
### Homebrew (recommended)
32+
33+
```bash
34+
brew install gomantics/tap/clipx
35+
```
36+
37+
### Go install
2738

2839
```bash
2940
go install github.com/gomantics/clipx/cmd/clipx@latest
3041
```
3142

32-
Or from source:
43+
### From source
3344

3445
```bash
3546
git clone https://github.com/gomantics/clipx.git
3647
cd clipx
3748
make build # binary at ./clipx-bin
3849
```
3950

40-
### 2. Pair them
51+
### Download binary
52+
53+
Pre-built binaries for macOS (Intel & Apple Silicon) are available on the [releases page](https://github.com/gomantics/clipx/releases/latest).
54+
55+
## Quick start
56+
57+
### 1. Pair your Macs
4158

4259
On **Mac A** (e.g. 192.168.0.5):
4360

@@ -51,7 +68,7 @@ On **Mac B** (e.g. 192.168.0.6):
5168
clipx pair 192.168.0.5 # IP of Mac A
5269
```
5370

54-
### 3. Install and run
71+
### 2. Install and run
5572

5673
On **both Macs**:
5774

@@ -144,29 +161,39 @@ One port, UDP only. If you run a firewall, `clipx install` handles it automatica
144161

145162
### Protocol
146163

147-
All communication is UDP unicast on port 9877. Three message types:
164+
All communication is UDP unicast on port 9877. Four message types:
148165

149166
| Type | Byte | Purpose |
150167
|---|---|---|
151-
| Clipboard | `C` | Carries clipboard content to peers |
168+
| Clipboard | `C` | Carries clipboard content (≤1300 bytes) |
169+
| Chunk | `K` | Carries a chunk of large clipboard content |
152170
| Ping | `P` | Health check request |
153171
| Pong | `A` | Health check response |
154172

155173
Wire format: `[6B magic "CLIPX2"] [1B type] [8B nodeID] [payload]`
156174

157175
Clipboard payload: `[64B SHA-256 hex hash] [clipboard data]`
158176

177+
Chunk payload: `[64B SHA-256 hex hash] [2B chunk index] [2B total chunks] [chunk data]`
178+
159179
### Loop prevention
160180

161181
1. Every clipboard write is hashed (SHA-256)
162182
2. When content arrives from a peer, its hash is recorded
163183
3. When the local clipboard watcher detects a change, it checks if the hash matches a recently received peer hash — if so, it skips broadcasting
164184

185+
### Reliability
186+
187+
- Small clipboard content (≤1300 bytes) is sent 3 times for UDP reliability
188+
- Large content is automatically chunked and reassembled on the receiver
189+
- Incomplete chunk transfers are cleaned up after 30 seconds
190+
- Persistent UDP connections to peers with automatic reconnection
191+
165192
### Limits
166193

167-
- Max clipboard: **10MB** (content >16KB is automatically chunked)
194+
- Max clipboard: **10 MB** (content >1300 bytes is automatically chunked)
168195
- Text only (uses `pbcopy`/`pbpaste`)
169-
- macOS only (for now)
196+
- macOS only
170197
- Peers must be on the same LAN
171198

172199
## Troubleshooting
@@ -188,6 +215,10 @@ Clipboard payload: `[64B SHA-256 hex hash] [clipboard data]`
188215
- LaunchAgent logs: `~/Library/Logs/clipx.log`
189216
- Live tail: `tail -f ~/Library/Logs/clipx.log`
190217

218+
## Contributing
219+
220+
Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
221+
191222
## License
192223

193224
MIT — see [LICENSE](LICENSE).

clipx/clipboard.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
// Package clipx implements LAN clipboard sync for macOS.
2+
//
3+
// It uses UDP unicast to send clipboard content between explicitly
4+
// paired peers on the same local network. Content is deduplicated
5+
// via SHA-256 hashing to prevent infinite ping-pong loops.
16
package clipx
27

38
import (
@@ -7,13 +12,17 @@ import (
712
"strings"
813
)
914

10-
// Clipboard abstracts clipboard read/write for testability.
15+
// Clipboard abstracts clipboard read/write operations.
16+
// Implementations must be safe for concurrent use.
1117
type Clipboard interface {
18+
// Read returns the current clipboard content.
1219
Read() ([]byte, error)
20+
// Write sets the clipboard content.
1321
Write(data []byte) error
1422
}
1523

16-
// MacClipboard uses pbcopy/pbpaste.
24+
// MacClipboard reads and writes the macOS system clipboard
25+
// using pbcopy and pbpaste.
1726
type MacClipboard struct{}
1827

1928
// utf8Env returns the current environment with LANG forced to UTF-8.

clipx/config.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import (
77
)
88

99
// Config holds persistent clipx configuration.
10+
// It is stored as JSON at ~/.config/clipx/config.json.
1011
type Config struct {
11-
Peers []string `json:"peers"` // list of peer IPs/hostnames
12+
Peers []string `json:"peers"` // IP addresses of paired peers
1213
}
1314

1415
// ConfigPath returns the path to the config file.

clipx/net.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ import (
66
"time"
77
)
88

9+
// DefaultPort is the UDP port used for all clipx communication
10+
// (clipboard sync, ping/pong health checks).
911
const DefaultPort = 9877
1012

11-
// ResolveAddr resolves a hostname or IP to an IP string.
13+
// ResolveAddr resolves a hostname or IP string to an IPv4 address.
14+
// If addr is already a valid IP, it is returned as-is.
1215
func ResolveAddr(addr string) (string, error) {
1316
// if it's already an IP, return as-is
1417
if ip := net.ParseIP(addr); ip != nil {
@@ -27,7 +30,8 @@ func ResolveAddr(addr string) (string, error) {
2730
return "", fmt.Errorf("no IPv4 address found for %s", addr)
2831
}
2932

30-
// PingPeer sends a ping and waits for a pong to check if a peer is reachable.
33+
// PingPeer sends a UDP ping to the given address and waits up to 1 second
34+
// for a pong response. Returns "● online" or "○ offline".
3135
func PingPeer(addr string) string {
3236
target := net.JoinHostPort(addr, fmt.Sprintf("%d", DefaultPort))
3337
conn, err := net.DialTimeout("udp4", target, 1*time.Second)

clipx/node.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,18 @@ const (
1515
pollInterval = 500 * time.Millisecond
1616
)
1717

18-
// Node is a clipx daemon instance.
18+
// Node is a clipx daemon instance that watches the local clipboard,
19+
// broadcasts changes to peers, and applies incoming clipboard content.
1920
type Node struct {
2021
id string
2122
peers []string // peer IPs
2223
clipboard Clipboard
2324
logger *log.Logger
24-
conn net.PacketConn // listener
25+
conn net.PacketConn // listener
2526
peerConns map[string]net.Conn // persistent send connections per peer
2627

27-
mu sync.Mutex
28-
lastHash string // last clipboard hash we've seen (sent or received)
28+
mu sync.Mutex
29+
lastHash string // last clipboard hash we've seen (sent or received)
2930

3031
// hashes received from peers — prevents re-broadcasting
3132
peerHashes map[string]time.Time
@@ -47,7 +48,7 @@ type chunkBuffer struct {
4748
createdAt time.Time
4849
}
4950

50-
// NewNode creates a new clipx node.
51+
// NewNode creates a new clipx node using the macOS system clipboard.
5152
func NewNode(cfg *Config, logger *log.Logger) (*Node, error) {
5253
return NewNodeWithClipboard(cfg, logger, &MacClipboard{})
5354
}
@@ -83,15 +84,18 @@ func NewNodeWithClipboard(cfg *Config, logger *log.Logger, cb Clipboard) (*Node,
8384
return n, nil
8485
}
8586

86-
// Start begins the listener, clipboard watcher, and maintenance.
87+
// Start launches three background goroutines: the UDP listener,
88+
// the clipboard poller, and the maintenance loop. Call [Node.Stop]
89+
// to shut down gracefully.
8790
func (n *Node) Start() {
8891
n.wg.Add(3)
8992
go n.listen()
9093
go n.watchClipboard()
9194
go n.maintenance()
9295
}
9396

94-
// Stop shuts down the node.
97+
// Stop gracefully shuts down the node, closing all connections
98+
// and waiting for goroutines to exit.
9599
func (n *Node) Stop() {
96100
close(n.stopCh)
97101
n.conn.Close()

clipx/protocol.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,14 @@ const hashLen = 64
3333
const chunkHeaderLen = hashLen + 2 + 2 // hash + index + total
3434

3535
const (
36-
// MaxChunkPayload is the max clipboard data per UDP packet.
37-
// Must stay under WiFi MTU (~1500) minus headers to avoid
38-
// "message too long" on macOS which sets DF (Don't Fragment).
39-
// 1500 MTU - 20 IP - 8 UDP - 15 clipx header - 68 chunk header = 1389
40-
MaxChunkPayload = 1300 // safe margin under any MTU
36+
// MaxChunkPayload is the maximum clipboard data per UDP packet.
37+
// Sized to stay under WiFi MTU (~1500 bytes) minus IP/UDP/clipx headers
38+
// to avoid "message too long" errors on macOS (which sets DF bit).
39+
// 1500 MTU - 20 IP - 8 UDP - 15 clipx header - 68 chunk header = 1389
40+
MaxChunkPayload = 1300 // conservative margin for any network
4141

42-
// MaxClipSize is the absolute max clipboard size we'll sync.
43-
MaxClipSize = 10 * 1024 * 1024 // 10MB
42+
// MaxClipSize is the maximum clipboard content size that will be synced.
43+
MaxClipSize = 10 * 1024 * 1024 // 10 MB
4444
)
4545

4646
// encodeMessage builds a wire message.

0 commit comments

Comments
 (0)