garclip
Clipboard Manager
An X11 clipboard manager with persistent history and fuzzy search. garclip monitors both CLIPBOARD (Ctrl+C) and PRIMARY (selection) using the XFixes extension for event-driven detection. Supports text and images with configurable filtering, deduplication, and a gartk-powered picker UI.
Features
- X11 clipboard monitoring (CLIPBOARD and PRIMARY)
- XFixes event-driven detection with polling fallback
- Persistent history with JSON storage
- Text and image content support
- Configurable max entries (default 1000)
- blake3 hash-based deduplication
- Pin entries to prevent removal
- Regex and window class filtering
- garclip-picker fuzzy search UI
- Unix socket IPC with garclipctl
- SIGHUP config reload
- Systemd user service support
- Auto-save every 5 minutes
- gartk-based picker rendering
Quick Start
What is garclip?
garclip is an X11 clipboard manager that maintains a persistent history of everything you copy. It runs as a daemon, monitoring clipboard changes and storing entries (text and images) to disk. Use garclip-picker or garclipctl to browse and restore previous clipboard contents.
Installation
Starting the Daemon
Basic Usage
gar Keybinding
Add to your ~/.config/gar/init.lua:
-- Mod+v to open clipboard picker
gar.bind("mod+v", function()
gar.exec("garclip-picker")
end) Configuration
Configuration file: ~/.config/garclip/config.toml
Complete Configuration
[history]
max_entries = 1000 # Maximum history entries
persist = true # Persist across restarts
# persist_path = "..." # Custom history file path
[behavior]
watch_primary = true # Watch PRIMARY selection (middle-click)
watch_clipboard = true # Watch CLIPBOARD (Ctrl+C)
deduplicate = true # Move duplicates to front instead of adding
ignore_empty = true # Ignore empty content
min_length = 1 # Minimum text length (bytes)
max_length = 10485760 # Maximum text length (10MB)
max_image_size = 52428800 # Maximum image size (50MB)
poll_interval_ms = 250 # Fallback polling interval
[filters]
ignore_patterns = [] # Regex patterns to ignore
ignore_classes = [] # Window classes to ignore
[daemon]
# socket_path = "..." # Custom socket path
log_level = "info" # Log level: error, warn, info, debug, trace
# log_file = "..." # Optional log file History Options
| Option | Type | Default | Description |
|---|---|---|---|
| max_entries | u32 | 1000 | Maximum history size (pinned entries can exceed) |
| persist | bool | true | Save history to disk |
| persist_path | string | ~/.local/share/garclip/history.json | Custom history file path |
Behavior Options
| Option | Type | Default | Description |
|---|---|---|---|
| watch_primary | bool | true | Monitor PRIMARY selection (text highlighting) |
| watch_clipboard | bool | true | Monitor CLIPBOARD selection (Ctrl+C) |
| deduplicate | bool | true | Move duplicates to front instead of adding new |
| ignore_empty | bool | true | Skip empty clipboard contents |
| min_length | u64 | 1 | Minimum text length in bytes |
| max_length | u64 | 10485760 | Maximum text length (10MB) |
| max_image_size | u64 | 52428800 | Maximum image size (50MB) |
| poll_interval_ms | u64 | 250 | Fallback polling interval (when XFixes unavailable) |
Filter Options
| Option | Type | Description |
|---|---|---|
| ignore_patterns | [string] | Regex patterns to filter out (case-insensitive) |
| ignore_classes | [string] | Window classes to ignore (substring match) |
Filter Examples
[filters]
# Ignore sensitive patterns
ignore_patterns = [
"^password:", # Lines starting with "password:"
"api[_-]?key", # API keys
"\\d{16}", # Credit card numbers (16 digits)
"^ssh-rsa ", # SSH keys
]
# Ignore password managers
ignore_classes = [
"keepass",
"bitwarden",
"1password",
] Overview
garclip is an X11 clipboard manager designed for the gardesk suite. It runs as a background daemon that monitors clipboard changes using the XFixes extension for event-driven detection (falling back to polling if unavailable).
The daemon stores both text and image content in a persistent history file, supporting up to 1000 entries by default. Entries can be pinned to prevent automatic removal, deduplicated via blake3 hashing, and filtered by regex patterns or source window class.
Module Structure
garclip/
├── garclip/ # Main daemon + library
│ ├── src/
│ │ ├── main.rs # CLI entry + daemon event loop
│ │ ├── clipboard/ # Entry types, history, manager, filtering
│ │ ├── config/ # TOML configuration
│ │ ├── daemon/ # DaemonState (state machine)
│ │ ├── x11/ # Atoms, selections, transfers, window info
│ │ └── ipc/ # Protocol, server, client, gar integration
├── garclipctl/ # CLI control tool
│ └── src/main.rs # Blocking IPC client (clap CLI)
└── garclip-picker/ # Graphical picker UI
└── src/
├── main.rs # Entry point + event loop
├── app.rs # Application state + input handling
└── ui.rs # Cairo/Pango rendering via gartk Key Dependencies
| Crate | Purpose |
|---|---|
| x11rb | X11 protocol (with xfixes feature) |
| tokio | Async runtime for daemon event loop |
| blake3 | Content hashing for deduplication |
| image | Image format support (png, jpeg, gif, webp) |
| gartk-* | Picker UI rendering (core, x11, render) |
| clap | CLI argument parsing (derive macros) |
Architecture
garclip is a Cargo workspace with three crates that work together:
| Crate | Binary | Role |
|---|---|---|
| garclip | garclip | Daemon process: monitors clipboard, stores history, serves IPC |
| garclipctl | garclipctl | CLI client: sends commands to daemon via Unix socket |
| garclip-picker | garclip-picker | GUI picker: gartk-rendered popup with fuzzy search |
The daemon runs a Tokio async event loop coordinating X11 monitoring, IPC serving, event broadcasting, auto-save, and signal handling. garclipctl and garclip-picker are standalone binaries that connect to the daemon's Unix socket.
Daemon Process
The main garclip daemon monitors X11 clipboard changes and maintains the history. It uses a Tokio async runtime with multiple concurrent tasks:
- XFixes monitor - Receives SelectionNotify events when clipboard owner changes
- Poll timer - 250ms fallback polling if XFixes unavailable
- IPC server - Accepts client connections via Unix socket, spawns per-client handler tasks
- Event broadcaster - Pushes events (clipboard_changed, entry_pinned, etc.) to subscribed clients
- Auto-save timer - Persists history to disk every 5 minutes
- Signal handler - SIGTERM/SIGINT for graceful shutdown, SIGHUP for config reload
DaemonState
The DaemonState struct in daemon/state.rs owns the ClipboardManager and executes all commands. It handles debounced PRIMARY selection (300ms delay to avoid capturing partial drag selections), processes X11 events, and persists/loads history on demand.
PID File
On startup, the daemon checks for an existing instance via $XDG_RUNTIME_DIR/garclip.pid. It writes a PID file on start and removes it on clean exit.
garclipctl CLI
Command-line interface for interacting with the daemon. Uses blocking IPC (no async runtime).
| Command | Description |
|---|---|
| copy [TEXT] | Copy text to clipboard (stdin if no arg) |
| paste | Get current clipboard content |
| history [-l N] | Show history (default 20 entries) |
| select ID | Restore entry to clipboard |
| delete ID | Delete entry from history |
| pin ID | Pin entry (prevent removal) |
| unpin ID | Unpin entry |
| pinned | List all pinned entries |
| search QUERY | Search history (text only) |
| clear | Clear clipboard |
| clear-history | Clear all history |
| status | Show daemon status |
| reload | Reload configuration |
| stop | Stop daemon gracefully |
Global flags: --socket PATH for custom socket, --json for machine-readable output.
garclip-picker UI
A graphical picker window rendered with gartk (Cairo/Pango). Opens as a 600x420px popup centered on the primary monitor with exclusive keyboard grab.
Keyboard Controls
| Key | Action |
|---|---|
| Up/Down, Ctrl+K/J | Navigate entries |
| Page Up/Down | Jump by page |
| Enter | Select entry (copy to clipboard, close) |
| Delete | Delete selected entry |
| Escape, Ctrl+C | Close without selecting |
| Ctrl+U | Clear search input |
| Home/End | Move cursor to start/end of search |
| Left/Right | Move cursor within search input |
| Backspace | Delete character before cursor |
| Type text | Filter entries by substring search |
Visual Layout
┌──────────────────────────────────┐
│ Search: [user input]▌ │
├──────────────────────────────────┤
│ * entry1 [pinned, 100ch preview] │
│ ▌ entry2 [selected, highlighted] │
│ entry3 [text preview...] │
│ entry4 [Image: image/png] │
│ entry5 [Copy: 2 files] │
│ ... │
└──────────────────────────────────┘ History Management
garclip maintains a clipboard history using a VecDeque<ClipboardEntry> with newest entries at the front. The history supports configurable limits, persistence, deduplication, and pinning.
| Feature | Config | Default |
|---|---|---|
| Max entries | history.max_entries | 1000 |
| Persistence | history.persist | true |
| Deduplication | behavior.deduplicate | true |
| Auto-save interval | - | 5 minutes |
Storage & Persistence
History is stored in JSON format at ~/.local/share/garclip/history.json. Each entry contains:
{
"id": 42,
"content": { "Text": "Hello world" },
"source": "firefox",
"timestamp": "2024-01-15T10:30:00Z",
"pinned": false,
"hash": "abc123..."
} Images are base64-encoded in the JSON with their MIME type. File URIs are stored as string arrays with a cut/copy flag.
History is saved on daemon shutdown (SIGTERM/SIGINT), every 5 minutes via auto-save, and on explicit garclipctl clear-history. On load, the next ID counter is rebuilt from the highest existing ID.
Deduplication
When deduplicate = true, copying the same content moves the existing entry to the front instead of creating a duplicate. Uses blake3 hashing for fast comparison.
How It Works
- Calculate
blake3(content)for new clipboard data - Check the in-memory
HashSet<String>for existing hash - If found: remove old entry, update timestamp, re-add to front
- If not found: create new entry with auto-incremented ID
This keeps frequently copied text at the top of history without creating duplicate entries.
Pinning Entries
Pinned entries are protected from automatic removal when history exceeds max_entries. Pin frequently used snippets to keep them always available.
X11 Integration
garclip interacts with X11 through several mechanisms for clipboard monitoring and content transfer. The X11 module handles atom interning, selection ownership, content negotiation, and window class identification.
| Module | Purpose |
|---|---|
| atoms.rs | Cached X11 atom interning (TARGETS, UTF8_STRING, image types, etc.) |
| selection.rs | Selection ownership and XFixes monitoring |
| transfer.rs | Content negotiation (TARGETS → convert → read property, INCR protocol) |
| window_info.rs | Window class/instance identification for filtering |
CLIPBOARD vs PRIMARY
X11 has multiple clipboard selections. garclip monitors both by default:
| Selection | Trigger | Config |
|---|---|---|
| CLIPBOARD | Ctrl+C, explicit copy | watch_clipboard |
| PRIMARY | Text highlighting, middle-click paste | watch_primary |
PRIMARY selection is debounced by primary_debounce_ms (default 300ms) to avoid capturing partial text during drag selection.
XFixes Extension
garclip uses the XFixes extension (version 5.0+) for event-driven clipboard monitoring. When a selection owner changes, XFixes sends a SelectionNotify event immediately rather than requiring polling.
How It Works
- Create a hidden input-only window for selection ownership
- Subscribe via
xfixes_select_selection_input()for SetSelectionOwner, WindowDestroy, ClientClose events - On SelectionNotify: request TARGETS from new owner, negotiate content format, convert and read
- When garclip sets clipboard: call
claim_ownership(CLIPBOARD)and handle incoming SelectionRequest events
If XFixes is unavailable, garclip falls back to polling at poll_interval_ms (default 250ms).
Content Negotiation
The TransferManager requests TARGETS first, then picks the best available format. Priority: files (gnome-copied-files, text/uri-list) > images (image/png, jpeg, gif, webp, bmp) > text (UTF8_STRING, text/plain, STRING). Large transfers use the INCR protocol with 1MB chunks.
Content Filtering
Filter clipboard content by pattern or source window to avoid storing sensitive data:
- ignore_patterns - Regex patterns (case-insensitive) matched against text content
- ignore_classes - Window class substring matching (case-insensitive)
- min_length / max_length - Text size bounds (default 1 byte to 10MB)
- max_image_size - Image size limit (default 50MB)
- ignore_empty - Skip empty/whitespace-only content
Filters are applied before adding entries to history. File URIs are never filtered.
Reload filters without restart: garclipctl reload or kill -HUP $(pgrep garclip).
Image Support
garclip stores images in their original format. Supported X11 targets and MIME types:
| MIME Type | X11 Target Atom |
|---|---|
| image/png | image/png |
| image/jpeg | image/jpeg |
| image/gif | image/gif |
| image/webp | image/webp |
| image/bmp | image/bmp |
Images are base64-encoded in the history JSON file. The max_image_size config limits stored image size (default 50MB).
File URI Support
Copied files (e.g., from a file manager) are stored as URI lists. garclip parses both text/uri-list and x-special/gnome-copied-files formats, preserving the cut/copy distinction.
Picker Interface
The garclip-picker renders a popup window using gartk's Cairo/Pango backend. It fetches the current history from the daemon, displays entries with content type indicators and pinned status, and supports real-time fuzzy filtering.
Rendering Pipeline
- Clear background, draw rounded border (gartk_render)
- Draw search input field with cursor
- For each visible entry (scroll_offset to scroll_offset + max_visible):
- Draw selection highlight if active
- Draw pinned indicator (*) and content type icon
- Draw preview text truncated to 100 chars
- Blit to X11 window via graphics context
Window Properties
| Property | Value |
|---|---|
| Size | 600 x 420 px |
| Position | Centered on primary monitor (1/3 from top) |
| Window class | garclip-picker |
| Type | Popup (RGBA visual, transparent) |
| Input | Exclusive keyboard grab (10 attempts, 50ms retry) |
Common Workflows
Paste Previous Item
Open the picker, select an older entry, and it becomes the current clipboard:
Search History
Find an entry by content:
Pin Frequently Used Snippets
Pin entries you use often so they don't get removed:
Copy from Script
Pipe content to garclipctl:
Clear Sensitive Data
Remove all history while keeping pinned entries:
gar Integration
Bind garclip-picker to a key:
-- Mod+v to open clipboard picker
gar.bind("mod+v", function()
gar.exec("garclip-picker")
end)
-- Mod+Shift+v to show history in terminal
gar.bind("mod+shift+v", function()
gar.exec("alacritty -e garclipctl history -l 50")
end) IPC & API Reference
garclip provides IPC via Unix domain sockets. The protocol uses newline-delimited JSON.
Socket Location
$XDG_RUNTIME_DIR/garclip.sock Fallback: /tmp/garclip.sock
IPC Commands
| Command | Args | Description |
|---|---|---|
| copy | text | Copy text to clipboard |
| paste | - | Get current clipboard |
| history | limit? | Get history entries |
| select | id | Restore entry to clipboard |
| delete | id | Delete entry |
| pin | id | Pin entry |
| unpin | id | Unpin entry |
| clear | - | Clear clipboard |
| clearHistory | keep_pinned? | Clear history |
| search | query, limit? | Search history |
| status | - | Daemon status |
| reload | - | Reload config |
| quit | - | Stop daemon |
| subscribe | events[] | Subscribe to events |
Request Format
{
"command": "history",
"limit": 20
} Response Format
{
"success": true,
"data": [...],
"error": null
} Entry Object
{
"id": 42,
"content": { "Text": "Hello world" },
"source": "firefox",
"timestamp": "2024-01-15T10:30:00Z",
"pinned": false,
"hash": "abc123def456..."
} Event Types
| Event | Description |
|---|---|
| clipboard_changed | New clipboard content captured |
| history_cleared | History was cleared |
| entry_pinned | Entry was pinned |
| entry_unpinned | Entry was unpinned |
| entry_deleted | Entry was deleted |
| entry_selected | Entry was selected/restored |
Troubleshooting
Daemon not starting
Check if already running:
garclipctl connection refused
Verify daemon is running and socket exists:
Clipboard not being captured
Check X11 connection and XFixes:
Content being filtered unexpectedly
Check filter configuration:
garclip-picker won't open
Check X11 and daemon connection:
History not persisting
Check persistence settings and file: