Skip to content

Instantly share code, notes, and snippets.

@hizkifw
Last active November 4, 2025 08:21
Show Gist options
  • Save hizkifw/98a0353b4e6f5638e30e949b900abc0f to your computer and use it in GitHub Desktop.
Save hizkifw/98a0353b4e6f5638e30e949b900abc0f to your computer and use it in GitHub Desktop.
#!/bin/bash
# Clipboard synchronization script for X11/Xwayland to Wayland
# Monitors X11 clipboard and syncs to Wayland when it changes
# Note: wl-clip-persist is required (https://github.com/Linus789/wl-clip-persist)
# Prevent multiple instances
# Use XDG_RUNTIME_DIR if available, fallback to /run/user/$UID, then /tmp
if [ -n "$XDG_RUNTIME_DIR" ] && [ -d "$XDG_RUNTIME_DIR" ]; then
LOCKFILE="$XDG_RUNTIME_DIR/clipboard-sync.lock"
elif [ -d "/run/user/$UID" ]; then
LOCKFILE="/run/user/$UID/clipboard-sync.lock"
else
LOCKFILE="/tmp/clipboard-sync-$UID.lock"
fi
exec 200>"$LOCKFILE"
if ! flock -n 200; then
echo "Another instance is already running. Exiting."
exit 1
fi
# Cleanup on exit
trap "rm -f '$LOCKFILE'" EXIT
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Function to log messages
log() {
echo -e "${GREEN}[$(date '+%H:%M:%S')]${NC} $1"
}
error() {
echo -e "${RED}[$(date '+%H:%M:%S')] ERROR:${NC} $1" >&2
}
warn() {
echo -e "${YELLOW}[$(date '+%H:%M:%S')] WARN:${NC} $1"
}
debug() {
if [ "$DEBUG" = "1" ]; then
echo -e "${BLUE}[$(date '+%H:%M:%S')] DEBUG:${NC} $1"
fi
}
# Check if required tools are available
if ! command -v xclip &> /dev/null; then
error "xclip is not installed. Please install it: sudo pacman -S xclip"
exit 1
fi
if ! command -v wl-paste &> /dev/null || ! command -v wl-copy &> /dev/null; then
error "wl-clipboard is not installed. Please install it: sudo pacman -S wl-clipboard"
exit 1
fi
if ! command -v timeout &> /dev/null; then
error "timeout command not found. Please install coreutils."
exit 1
fi
log "Starting clipboard synchronization (X11 -> Wayland)"
log "Press Ctrl+C to stop"
# Initialize clipboard states with hash to avoid storing large content
last_x11_hash=""
last_wayland_hash=""
last_synced_hash=""
# Sleep interval in seconds
SLEEP_INTERVAL=0.3
TIMEOUT=0.2
# Function to get clipboard hash
get_hash() {
echo -n "$1" | md5sum | cut -d' ' -f1
}
# Function to get hash from binary data
get_binary_hash() {
md5sum | cut -d' ' -f1
}
# Function to get X11 clipboard MIME types
get_x11_types() {
timeout "$TIMEOUT" xclip -selection clipboard -t TARGETS -o 2>/dev/null | grep -v "^TARGETS$"
}
# Function to get Wayland clipboard MIME types
get_wayland_types() {
timeout "$TIMEOUT" wl-paste --list-types 2>/dev/null
}
# Function to select best MIME type for syncing
select_mime_type() {
local types="$1"
# Priority order: images first, then text
if echo "$types" | grep -q "image/png"; then
echo "image/png"
elif echo "$types" | grep -q "image/jpeg"; then
echo "image/jpeg"
elif echo "$types" | grep -q "image/jpg"; then
echo "image/jpg"
elif echo "$types" | grep -q "image/"; then
echo "$types" | grep "^image/" | head -n1
elif echo "$types" | grep -q "text/plain"; then
echo "text/plain"
elif echo "$types" | grep -q "UTF8_STRING"; then
echo "UTF8_STRING"
elif echo "$types" | grep -q "STRING"; then
echo "STRING"
elif echo "$types" | grep -q "TEXT"; then
echo "TEXT"
else
# Return first available type
echo "$types" | head -n1
fi
}
while true; do
# Get X11 clipboard MIME types
x11_types=$(get_x11_types)
x11_types_exit=$?
if [ $x11_types_exit -eq 124 ]; then
warn "xclip (TARGETS) timed out"
sleep "$SLEEP_INTERVAL"
continue
fi
# Get Wayland clipboard MIME types
wayland_types=$(get_wayland_types)
wayland_types_exit=$?
# Determine X11 clipboard type and content
if [ $x11_types_exit -eq 0 ] && [ -n "$x11_types" ]; then
x11_mime=$(select_mime_type "$x11_types")
if [[ "$x11_mime" == image/* ]]; then
# Handle image content
debug "X11 clipboard contains image: $x11_mime"
x11_clipboard=$(timeout "$TIMEOUT" xclip -selection clipboard -t "$x11_mime" -o 2>/dev/null | base64 -w 0)
x11_exit_code=$?
x11_is_image=1
else
# Handle text content
debug "X11 clipboard contains text: $x11_mime"
x11_clipboard=$(timeout "$TIMEOUT" xclip -selection clipboard -o 2>/dev/null)
x11_exit_code=$?
x11_is_image=0
fi
else
x11_clipboard=""
x11_exit_code=1
x11_mime=""
x11_is_image=0
fi
if [ $x11_exit_code -eq 124 ]; then
warn "xclip timed out"
sleep "$SLEEP_INTERVAL"
continue
fi
# Determine Wayland clipboard type and content
if [ $wayland_types_exit -eq 0 ] && [ -n "$wayland_types" ]; then
wayland_mime=$(select_mime_type "$wayland_types")
if [[ "$wayland_mime" == image/* ]]; then
# Handle image content
debug "Wayland clipboard contains image: $wayland_mime"
wayland_clipboard=$(timeout "$TIMEOUT" wl-paste -t "$wayland_mime" 2>/dev/null | base64 -w 0)
wayland_exit_code=$?
else
# Handle text content
wayland_clipboard=$(timeout "$TIMEOUT" wl-paste 2>/dev/null)
wayland_exit_code=$?
fi
else
wayland_clipboard=""
wayland_exit_code=1
wayland_mime=""
fi
# Calculate X11 hash
if [ -n "$x11_clipboard" ]; then
x11_hash=$(get_hash "$x11_clipboard")
else
x11_hash=""
fi
# Handle wl-paste timeout - assume clipboard didn't change
if [ $wayland_exit_code -eq 124 ]; then
debug "wl-paste timed out, assuming Wayland clipboard unchanged"
wayland_hash="$last_wayland_hash"
else
# Calculate wayland hash only if wl-paste succeeded
if [ -n "$wayland_clipboard" ]; then
wayland_hash=$(get_hash "$wayland_clipboard")
else
wayland_hash=""
fi
fi
debug "X11: $x11_hash ($x11_mime), Wayland: $wayland_hash ($wayland_mime), Last synced: $last_synced_hash"
# Check if Wayland clipboard changed externally (not from our sync)
# Skip this check if wl-paste timed out
if [ $wayland_exit_code -ne 124 ]; then
if [ -n "$wayland_hash" ] && [ "$wayland_hash" != "$last_wayland_hash" ] && [ "$wayland_hash" != "$last_synced_hash" ]; then
log "Wayland clipboard changed externally (clearing sync flag)"
last_wayland_hash="$wayland_hash"
# Clear the last synced hash so next X11 copy will trigger sync even if same content
last_synced_hash=""
fi
fi
# Check if X11 clipboard has changed
if [ $x11_exit_code -eq 0 ] && [ -n "$x11_clipboard" ] && [ -n "$x11_hash" ]; then
if [ "$x11_hash" != "$last_x11_hash" ]; then
if [ $x11_is_image -eq 1 ]; then
log "X11 clipboard changed (image: $x11_mime), syncing to Wayland..."
else
log "X11 clipboard changed (text), syncing to Wayland..."
debug "Content preview: ${x11_clipboard:0:50}..."
fi
# Copy X11 clipboard to Wayland with timeout
if [ $x11_is_image -eq 1 ]; then
# Sync image content
if echo -n "$x11_clipboard" | base64 -d | timeout "$TIMEOUT" wl-copy -t "$x11_mime" 2>/dev/null; then
log "✓ Synced image to Wayland clipboard"
last_x11_hash="$x11_hash"
last_synced_hash="$x11_hash"
last_wayland_hash="$x11_hash"
else
error "Failed to sync image to Wayland clipboard"
fi
else
# Sync text content
if echo -n "$x11_clipboard" | timeout "$TIMEOUT" wl-copy 2>/dev/null; then
log "✓ Synced text to Wayland clipboard"
last_x11_hash="$x11_hash"
last_synced_hash="$x11_hash"
last_wayland_hash="$x11_hash"
else
error "Failed to sync text to Wayland clipboard"
fi
fi
elif [ "$x11_hash" = "$last_x11_hash" ] && [ "$x11_hash" != "$last_synced_hash" ]; then
# Same X11 content as before, but it wasn't synced last time (because Wayland changed)
# Re-sync it now
if [ $x11_is_image -eq 1 ]; then
log "Re-syncing same X11 image to Wayland..."
if echo -n "$x11_clipboard" | base64 -d | timeout "$TIMEOUT" wl-copy -t "$x11_mime" 2>/dev/null; then
log "✓ Re-synced image to Wayland clipboard"
last_synced_hash="$x11_hash"
last_wayland_hash="$x11_hash"
else
error "Failed to re-sync image to Wayland clipboard"
fi
else
log "Re-syncing same X11 text to Wayland..."
if echo -n "$x11_clipboard" | timeout "$TIMEOUT" wl-copy 2>/dev/null; then
log "✓ Re-synced text to Wayland clipboard"
last_synced_hash="$x11_hash"
last_wayland_hash="$x11_hash"
else
error "Failed to re-sync text to Wayland clipboard"
fi
fi
fi
fi
sleep "$SLEEP_INTERVAL"
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment