first commit

This commit is contained in:
Space-Banane
2026-01-17 15:19:07 +01:00
commit 4e637bc27d
15 changed files with 511 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
config.go
.env
*.exe

65
Readme.md Normal file
View File

@@ -0,0 +1,65 @@
# Clipboard on the GO 😉
A simple clipboard synchronization tool built with Go. It consists of a server and a client application that work together to keep your clipboard data in sync across multiple devices.
## Features
- Real-time clipboard synchronization
- Cross-platform support (Windows, macOS, Linux)
- Easy to set up and use
## Prerequisites
- Go programming language installed (version 1.16 or higher)
- Git installed
## Installation
1. Clone the repository:
```bash
git clone https://gitea.reversed.dev/space/clipboard-on-the-go.git
cd clipboard-on-the-go
```
2. Modify the configuration.
2.1 Server Configuration:
- Navigate to the `server` directory.
- Make a new file called `config.go` and paste/modify the following code:
```go
package main
const (
// Update these values before building
EmbeddedServerIP = "0.0.0.0"
EmbeddedServerPort = "8080"
)
```
2.2 Client Configuration:
- Navigate to the `client` directory.
- Make a new file called `config.go` and paste/modify the following code:
```go
package main
const (
// Update these values before building
EmbeddedIdentification = "my-unique-id"
EmbeddedServerIP = "100.107.73.38"
EmbeddedServerPort = "8080"
)
```
3. Build the programs.
- Run the `build.bat` script in the root directory to build both the server and client applications.
4. Run the server.
- Navigate to the `server` directory and execute the `clipboard-sync-server.exe` file.
5. Run the client.
- Navigate to the `client` directory and execute the `clipboard-sync.exe` file.
## Usage
This is intended to be hosted on a public ish server, preferably in a tailnet or via a VPN, as you never should expose anything to the internet.
I use this on my home server, which i can connect to with tailscale, so i don't have to worry about exposing it to the internet.
Clients are identified by the `EmbeddedIdentification` value in the client config file. Make sure to set this to a unique value for each client you want to sync.
## License
This project is licensed under the MIT License.
## Disclaimer
This software is provided "as is", without warranty of any kind. Use at your own risk.

5
build-client.bat Normal file
View File

@@ -0,0 +1,5 @@
@echo off
cd client
go build -ldflags -H=windowsgui -o clipboard-sync.exe sync.go config.go icon.go
echo Build complete: clipboard-sync.exe
pause

5
build-server.bat Normal file
View File

@@ -0,0 +1,5 @@
@echo off
cd server
go build -o clipboard-sync-server.exe server.go config.go
echo Build complete: clipboard-sync-server.exe
pause

10
build.bat Normal file
View File

@@ -0,0 +1,10 @@
@echo off
call build-server.bat
if errorlevel 1 exit /b 1
cd ..
call build-client.bat
if errorlevel 1 exit /b 1
cd ..
echo Build completed successfully.

26
client/embedicon.go Normal file
View File

@@ -0,0 +1,26 @@
//go:build ignore
// +build ignore
package main
import (
"fmt"
"os"
)
func main() {
data, err := os.ReadFile("../syncup.ico")
if err != nil {
fmt.Println("Error reading icon:", err)
return
}
fmt.Println("var EmbeddedIcon = []byte{")
for i, b := range data {
if i%12 == 0 {
fmt.Print("\n\t")
}
fmt.Printf("0x%02X, ", b)
}
fmt.Println("\n}")
}

25
client/go.mod Normal file
View File

@@ -0,0 +1,25 @@
module clipboard-on-the-go
go 1.25.5
require (
github.com/joho/godotenv v1.5.1
golang.design/x/clipboard v0.7.1
)
require (
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
github.com/getlantern/systray v1.2.2 // indirect
github.com/go-stack/stack v1.8.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476 // indirect
golang.org/x/image v0.28.0 // indirect
golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f // indirect
golang.org/x/sys v0.33.0 // indirect
)

42
client/go.sum Normal file
View File

@@ -0,0 +1,42 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE=
github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.design/x/clipboard v0.7.1 h1:OEG3CmcYRBNnRwpDp7+uWLiZi3hrMRJpE9JkkkYtz2c=
golang.design/x/clipboard v0.7.1/go.mod h1:i5SiIqj0wLFw9P/1D7vfILFK0KHMk7ydE72HRrUIgkg=
golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476 h1:Wdx0vgH5Wgsw+lF//LJKmWOJBLWX6nprsMqnf99rYDE=
golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8=
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f h1:/n+PL2HlfqeSiDCuhdBbRNlGS/g2fM4OHufalHaTVG8=
golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f/go.mod h1:ESkJ836Z6LpG6mTVAhA48LpfW/8fNR0ifStlH2axyfg=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=

10
client/icon.go Normal file
View File

@@ -0,0 +1,10 @@
package main
// EmbeddedIcon contains the icon data
// To generate this, run: go run embedicon.go
var EmbeddedIcon = []byte{
// Replace this with your actual icon bytes
// You can use a tool or script to convert syncup.ico to byte array
// For now, using minimal PNG as placeholder
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
}

144
client/sync.go Normal file
View File

@@ -0,0 +1,144 @@
package main
import (
"bytes"
"fmt"
"log"
"github.com/getlantern/systray"
"github.com/gorilla/websocket"
"golang.design/x/clipboard"
)
var (
conn *websocket.Conn
lastClipboard []byte
identification string
isConnected bool
)
func onReady() {
// Use embedded icon
systray.SetIcon(EmbeddedIcon)
systray.SetTitle("Clipboard Sync")
systray.SetTooltip("Clipboard synchronization active")
mStatus := systray.AddMenuItem("Status: Connecting...", "Connection status")
mStatus.Disable()
systray.AddSeparator()
mQuit := systray.AddMenuItem("Quit", "Quit the application")
// Start clipboard monitoring
go monitorClipboard()
// Start client connection
go startClient(mStatus)
// Handle menu interactions
go func() {
for {
select {
case <-mQuit.ClickedCh:
systray.Quit()
}
}
}()
}
func onExit() {
if conn != nil {
conn.Close()
}
log.Println("Exiting clipboard sync client")
}
func startClient(statusItem *systray.MenuItem) {
// Use embedded configuration
identification = EmbeddedIdentification
server_ip := EmbeddedServerIP
server_port := EmbeddedServerPort
server_connection := "ws://" + server_ip + ":" + server_port + "/ws"
if identification == "" || server_ip == "" || server_port == "" {
log.Println("Missing configuration")
statusItem.SetTitle("Status: Config error")
return
}
// Connect to the server
var connectErr error
conn, _, connectErr = websocket.DefaultDialer.Dial(server_connection, nil)
if connectErr != nil {
log.Println("Error connecting to server:", connectErr)
statusItem.SetTitle("Status: Connection failed")
return
}
// Send identification to the server
sendMessage(conn, websocket.TextMessage, []byte(identification))
isConnected = true
statusItem.SetTitle(fmt.Sprintf("Status: Connected (%s)", identification))
log.Println("Connected to server")
// Listen for messages from the server
for {
messageType, message, err := conn.ReadMessage()
if err != nil {
log.Println("Error reading message:", err)
isConnected = false
statusItem.SetTitle("Status: Disconnected")
return
}
handleMessage(messageType, message)
}
}
func handleMessage(messageType int, message []byte) {
log.Printf("Received clipboard update: %d bytes", len(message))
// Update local clipboard tracking to avoid re-sending
lastClipboard = make([]byte, len(message))
copy(lastClipboard, message)
// Write to clipboard
clipboard.Write(clipboard.FmtText, message)
log.Println("Clipboard updated")
}
func sendMessage(conn *websocket.Conn, messageType int, message []byte) {
err := conn.WriteMessage(messageType, message)
if err != nil {
log.Println("Error sending message:", err)
}
}
func monitorClipboard() {
for {
if isConnected && conn != nil {
data := clipboard.Read(clipboard.FmtText)
// Check if clipboard has changed
if len(data) > 0 && !bytes.Equal(data, lastClipboard) {
log.Printf("Clipboard changed, sending to server: %d bytes", len(data))
sendMessage(conn, websocket.TextMessage, data)
lastClipboard = make([]byte, len(data))
copy(lastClipboard, data)
}
}
}
}
func getDefaultIcon() []byte {
return EmbeddedIcon
}
func main() {
// Initialize clipboard
clip_err := clipboard.Init()
if clip_err != nil {
log.Fatal("Failed to initialize clipboard:", clip_err)
}
// Run systray application
systray.Run(onReady, onExit)
}

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

7
server/go.mod Normal file
View File

@@ -0,0 +1,7 @@
module server
go 1.25.5
require github.com/gorilla/websocket v1.5.3
require github.com/joho/godotenv v1.5.1 // indirect

4
server/go.sum Normal file
View File

@@ -0,0 +1,4 @@
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=

165
server/server.go Normal file
View File

@@ -0,0 +1,165 @@
package main
import (
"log"
"net/http"
"sync"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
type Client struct {
conn *websocket.Conn
identification string
send chan []byte
}
type Server struct {
clients map[*Client]bool
register chan *Client
unregister chan *Client
broadcast chan *Message
mu sync.RWMutex
}
type Message struct {
identification string
data []byte
sender *Client
}
func newServer() *Server {
return &Server{
clients: make(map[*Client]bool),
register: make(chan *Client),
unregister: make(chan *Client),
broadcast: make(chan *Message),
}
}
func (s *Server) run() {
for {
select {
case client := <-s.register:
s.mu.Lock()
s.clients[client] = true
s.mu.Unlock()
log.Printf("Client registered with identification: %s", client.identification)
case client := <-s.unregister:
s.mu.Lock()
if _, ok := s.clients[client]; ok {
delete(s.clients, client)
close(client.send)
log.Printf("Client unregistered: %s", client.identification)
}
s.mu.Unlock()
case message := <-s.broadcast:
s.mu.RLock()
for client := range s.clients {
if client.identification == message.identification && client != message.sender {
select {
case client.send <- message.data:
default:
close(client.send)
delete(s.clients, client)
}
}
}
s.mu.RUnlock()
}
}
}
func (c *Client) readPump(server *Server) {
defer func() {
server.unregister <- c
c.conn.Close()
}()
for {
_, message, err := c.conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Printf("error: %v", err)
}
break
}
log.Printf("Received clipboard data from %s: %d bytes", c.identification, len(message))
server.broadcast <- &Message{
identification: c.identification,
data: message,
sender: c,
}
}
}
func (c *Client) writePump() {
defer c.conn.Close()
for message := range c.send {
sendMessage(c.conn, websocket.TextMessage, message)
log.Printf("Sent clipboard data to %s", c.identification)
}
}
func sendMessage(conn *websocket.Conn, messageType int, message []byte) {
err := conn.WriteMessage(messageType, message)
if err != nil {
log.Println("Error sending message:", err)
}
}
func handleWebSocket(server *Server, w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("Error upgrading connection:", err)
return
}
// First message should be identification
_, identification, err := conn.ReadMessage()
if err != nil {
log.Println("Error reading identification:", err)
conn.Close()
return
}
client := &Client{
conn: conn,
identification: string(identification),
send: make(chan []byte, 256),
}
server.register <- client
go client.writePump()
go client.readPump(server)
}
func main() {
port := EmbeddedServerPort
if port == "" {
port = "8080"
}
server := newServer()
go server.run()
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
handleWebSocket(server, w, r)
})
log.Printf("Server starting on port %s", port)
err := http.ListenAndServe(":"+port, nil)
if err != nil {
log.Fatal("ListenAndServe error:", err)
}
}

BIN
syncup.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB