RaΓΊl Pleitez

Build a cross-platform clipboard manager: Part 1 - High Level Design

Building a cross-platform clipboard manager presents unique challenges, especially when dealing with different operating systems' native APIs. In this series, we'll develop cclip, a clipboard manager from scratch that works across macOS, Linux, and Windows. We'll use Go as our primary language, leveraging CGO to interface with native APIs through C and Objective-C.

This first part focuses on establishing the foundation for our cross-platform implementation, setting up the project structure, and defining clean functions that will allow us to implement platform-specific behaviors while maintaining a consistent API across our application.

Project Overview

cclip is designed to be a lightweight, efficient clipboard manager that runs in the background, monitoring and storing clipboard changes while providing a simple API for clipboard operations. Key features include:

  • Clean API for clipboard operations (Read, Write, Watch)
  • Real-time clipboard monitoring
    • Initially with a web-based UI using Server-Sent Events (SSE) for real-time updates
    • Later, once the core functionality is completed, native UIs for each platform
  • Support for text and images
  • Persistent clipboard history storage
  • History full-text search
  • Smart clipboard history search using LLMs πŸͺ„
    • Integration with local LLM models served by Ollama
    • Context-aware searches for images and text

Architecture

The clipboard manager is designed both as a standalone application and as a reusable Go package. It leverages Go's build constraints through filename conventions to implement platform-specific behaviors while providing a clean, importable API for other projects. The project is structured around these main components:

  • Platform Agnostic Core (cclip.go): Defines the package's public API and orchestrates clipboard operations across platforms, making it importable by other Go projects
  • Platform Bridges: CGO-based implementations for each OS (cclip_darwin.go, cclip_linux.go, cclip_windows.go)
  • Storage Layer (store/): Handles persistent clipboard history
  • LLM Integration (llm/): Manages interactions with Ollama for smart searches
  • Main Application (cmd/cclip/): Entry point to the cclip clipboard manager application
 1cclip/
 2β”œβ”€β”€ cclip.go          # Platform-agnostic public API
 3β”œβ”€β”€ cclip_darwin.go   # macOS native implementation
 4β”œβ”€β”€ cclip_linux.go    # Linux native implementation
 5β”œβ”€β”€ cclip_windows.go  # Windows native implementation
 6β”œβ”€β”€ cmd/
 7β”‚   └── cclip/
 8β”‚       └── main.go   # Application entry point
 9β”œβ”€β”€ go.mod
10β”œβ”€β”€ store/
11β”‚   └── store.go      # Clipboard history persistence
12└── llm/
13    └── llm.go        # LLM integration for smart searches

Core Interface Design

At the heart of cclip is a set of simple, clear functions that abstract clipboard operations across different platforms. These package-level functions delegate to platform-specific implementations through build-constrained files.

The Public API

 1package cclip
 2
 3// Write data to the clipboard.
 4func Write(data []byte) error
 5
 6// Read the current content of the clipboard. If clipboard is empty,
 7// or there was an error reading, the returned value will be nil.
 8func Read() []byte
 9
10// Handler handles new data that was written to the clipboard.
11type Handler func(ctx context.Context, data []byte)
12
13// Watch for new changes in the clipboard. This is a blocking call until
14// context cancellation or error.
15func Watch(ctx context.Context, checkInterval time.Duration, handler Handler)

This API design offers:

  • Simple, straightforward functions for clipboard operations
  • Byte slices for maximum flexibility in data handling
  • Context-aware watching with customizable check intervals
  • Callback-based handling of clipboard changes

Usage Example

This is how the cclip package can be used. Exactly how we'll use it when implementing the standalone cclip app.

 1// Example usage of the package.
 2
 3// Write to clipboard.
 4data := []byte("Hello, clipboard!")
 5if err := cclip.Write(data); err != nil {
 6    log.Fatal(err)
 7}
 8
 9// Read current content.
10if content := cclip.Read(); content != nil {
11    fmt.Printf("Clipboard contains: %s\n", content)
12}
13
14// Watch for changes.
15ctx := context.Background()
16cclip.Watch(ctx, time.Second, func(ctx context.Context, data []byte) {
17    fmt.Printf("Clipboard changed: %s\n", data)
18})

Build and Run

Now that we have a clear understanding of cclip's architecture and public API, let's go through the steps to compile and run a basic version of our clipboard manager on different platforms. This will have us ready to start coding the implementation later.

Ensure you have Go installed. Download it from the official Go website.

Create Project

To start, create a new directory for your project and navigate into it:

1mkdir cclip
2cd cclip

Next, initialize a new Go module. This will set up the structure needed to manage dependencies:

1go mod init cclip

Now your project is ready for development! Let's start by creating the necessary files and directories in the next section.

Platform-Specific Implementations

Each supported platform (macOS, Linux, and Windows) provides its own implementation through build-constrained files. Create the file cclip/cclip_darwin.go with the following content:

 1package cclip
 2
 3import "fmt"
 4
 5func read() []byte {
 6    // macOS-specific implementation using native APIs.
 7    fmt.Println("macOS: clipboard read")
 8    return nil
 9}
10
11func write(data []byte) error {
12    // macOS-specific implementation using native APIs.
13    fmt.Println("macOS: clipboard write")
14    return nil
15}
16
17func watch(ctx context.Context, checkInterval time.Duration, handler Handler) {
18    // macOS-specific implementation using native APIs.
19    fmt.Println("macOS: clipboard watch")
20}

The Linux (cclip/cclip_linux.go) and Windows (cclip/cclip_windows.go) implementations follow the same pattern, each using their respective native APIs.

Create the files with the same content as the macOS one, except for the comments and log message. Update them accordingly.

Coordinating Platform-Specific Code in cclip.go

The package-level functions in cclip/cclip.go orchestrate the above platform-specific implementations. Create the file with the following content:

 1package cclip
 2
 3import (
 4    "context"
 5    "time"
 6)
 7
 8// Write data to the clipboard.
 9func Write(data []byte) error {
10    // Calls platform-specific implementation.
11    return write(data)
12}
13
14// Read the current content of the clipboard. If clipboard is empty,
15// or there was an error reading, the returned value will be nil.
16func Read() []byte {
17    // Calls platform-specific implementation.
18    return read()
19}
20
21// Handler handles new data that was written to the clipboard.
22type Handler func(ctx context.Context, data []byte)
23
24// Watch for new changes in the clipboard. This is a blocking call until
25// context cancellation or error.
26func Watch(ctx context.Context, checkInterval time.Duration, handler Handler) {
27    // Calls platform-specific implementation.
28    watch(ctx, checkInterval, handler)
29}

Application Entry Point

This is the entry point to the cclip application. Create the file cclip/cmd/cclip/main.go with the following content:

 1package main
 2
 3import (
 4    "context"
 5    "time"
 6
 7    "cclip"
 8)
 9
10func main() {
11    cclip.Read()
12    cclip.Write(nil)
13    cclip.Watch(context.TODO(), 500*time.Millisecond, nil)
14}

Run the cclip app, and see it output the messages of each clipboard operation:

1❯ go run ./cmd/cclip
2macOS: clipboard read
3macOS: clipboard write
4macOS: clipboard watch

Depending on the operating system you're running, you should get the platform-specific placeholder message we defined.

Next Steps

In the next post, β€œBuild a cross-platform clipboard manager: Part 2 – Native macOS support,” we'll dive into implementing the clipboard operations (Write, Read, and Watch) specifically for macOS. We'll leverage CGO to call native AppKit APIs written in C/Objective-C, bridging them with our Go code. By the end of Part 2, you'll have a fully functional macOS clipboard manager that seamlessly integrates with the native clipboard system.

Future parts will focus on adding Linux and Windows support, exploring storage and UI enhancements, and integrating local LLM models to achieve advanced, context-aware clipboard search. Stay tuned!