CLAUDE.mdrust

UniClipboard CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

View Source

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

uniclipboard-desktop is a cross-platform clipboard synchronization tool built with Tauri 2, React, and Rust. It enables real-time clipboard sharing between devices on LAN (WebSocket) and remotely (WebDAV), with XChaCha20-Poly1305 encryption for security.

Architecture Documentation

For detailed architecture design, interaction flows, and system overview, refer to the project's DeepWiki documentation:

  • URL: https://deepwiki.com/UniClipboard/UniClipboard
  • Access: Use mcp-deepwiki MCP server to query the documentation programmatically

This resource provides comprehensive diagrams, flow explanations, and design decisions that complement the code structure.

Development Commands

Core Development

# Install dependencies (uses Bun)
bun install

# Start development server (frontend on :1420, backend hot-reload)
bun tauri dev

# Build for production
bun tauri build

# Frontend-only development
bun run dev        # Start Vite dev server
bun run build      # Build frontend with TypeScript check
bun run preview    # Preview production build

Cross-Platform Building

Building is handled via GitHub Actions. Trigger manually from GitHub Actions tab with:

  • platform: macos-aarch64, macos-x86_64, ubuntu-22.04, windows-latest, or all
  • version: Version number (e.g., 1.0.0)

Cargo Command Location

CRITICAL: All Rust-related commands (cargo build, cargo test, cargo check, etc.) MUST be executed from src-tauri/.

# ✅ CORRECT - Always run from src-tauri/
cd src-tauri && cargo build
cd src-tauri && cargo test
cd src-tauri && cargo check

# ❌ FORBIDDEN - Never run from project root
cargo build
cargo test

Never run any Cargo command from the project root. If Cargo.toml is not present in the current directory, stop immediately and do not retry.

Test Coverage

Generate local coverage report using cargo-llvm-cov:

bun run test:coverage
open src-tauri/target/llvm-cov/html/index.html

Coverage is automatically uploaded to Codecov on each push/PR for tracking incremental changes.

Logging

Overview

The application uses tracing crate as the primary logging framework with structured logging and span-based context tracking.

Supported Features:

  • Spans - Structured context spans with parent-child relationships (e.g., tracing::info_span!)
  • Structured fields - Field-based logging with typed values
  • Event logging - tracing::info!, tracing::error!, etc.

See docs/architecture/logging-architecture.md for detailed architecture, span naming conventions, and configuration.

Configuration

Logging is initialized in src-tauri/src/main.rs using init_tracing_subscriber() from src-tauri/crates/uc-tauri/src/bootstrap/tracing.rs.

Environment Behavior

  • Development: Debug level, tracing::* outputs to terminal, legacy log::* outputs to Webview console
  • Production: Info level, tracing::* outputs to stdout, legacy log::* outputs to uniclipboard.log + stdout

Log File Locations

  • macOS: ~/Library/Logs/com.uniclipboard/uniclipboard.log
  • Linux: ~/.local/share/com.uniclipboard/logs/uniclipboard.log
  • Windows: %LOCALAPPDATA%\com.uniclipboard\logs/uniclipboard.log

Using Logs in Code

use tracing::{info, error, warn, debug, trace, info_span, Instrument};

pub fn my_function() {
    info!("Something happened");
    error!("Something went wrong: {}", error);
    debug!("Detailed debugging info");
}

// For async operations with spans
pub async fn my_async_function() {
    let span = info_span!("usecase.example.execute", param = %value);
    async move {
        info!("Processing...");
    }.instrument(span).await
}

Span Best Practices

CRITICAL: Understand the difference between Spans and Events:

  • Span = An operation's time range (has a beginning and end)
  • Event = Something that happens at a single moment in time

Correct Pattern: Use spans to represent each operation step, not just individual debug events:

// ❌ WRONG - Too many debug events, no span context
debug!("Deleting selection");
self.selection_repo.delete_selection(entry_id).await?;
debug!("Selection deleted");

// ✅ CORRECT - Use span to represent the operation
self.selection_repo
    .delete_selection(entry_id)
    .instrument(info_span!("delete_selection", entry_id = %entry_id))
    .await?;

// ✅ CORRECT - Span for async blocks with multiple steps
let entry = async {
    self.entry_repo
        .get_entry(entry_id)
        .await?
        .ok_or_else(|| anyhow::anyhow!("Entry not found: {}", entry_id))
}
.instrument(info_span!("fetch_entry", entry_id = %entry_id))
.await?;

Span Hierarchy Example:

usecase.delete_clipboard_entry.execute        ← #[instrument] auto-created
├── fetch_entry                               ← Manual span
├── delete_selection                          ← Manual span
├── delete_entry                              ← Manual span
└── delete_event                              ← Manual span

Key Benefits:

  • Spans automatically record operation start/end time
  • Tokio Console and log aggregators show complete call hierarchy
  • Reduces redundant log code
  • Each operation's duration is automatically tracked
  • Better for debugging async systems where multiple operations interleave

When to Use Events vs Spans:

  • Use events (info!, error!, etc.) for single-moment occurrences (errors, state changes)
  • Use spans (info_span! + .instrument()) for operations with duration
  • Use #[tracing::instrument] on functions to auto-create spans with parameters as fields

Sources:

Viewing Logs

Development:

  • Terminal: Logs appear in the terminal where bun tauri dev is running
  • Browser: Open DevTools (F12) → Console tab

Production:

  • Check the log file at the platform-specific location above
  • Run tail -f ~/Library/Logs/com.uniclipboard/uniclipboard.log (macOS) for live monitoring

Log Filtering

The logging system filters out:

  • libp2p_mdns errors below WARN level (harmless proxy software errors)
  • Tauri internal event logs to avoid infinite loops
  • ipc::request logs in production builds

See src-tauri/crates/uc-tauri/src/bootstrap/tracing.rs for tracing configuration and logging.rs for legacy log configuration.

Architecture

Backend (Rust with Tauri 2)

NOTE: The backend is currently undergoing a major refactoring from Clean Architecture to Hexagonal Architecture. Both old and new code coexist during the transition.

New Architecture (Target)

The new architecture follows Hexagonal Architecture (Ports and Adapters) with crate-based modularization:

src-tauri/crates/
├── uc-core/         # Core domain layer (90% complete)
│   ├── clipboard/   # Clipboard aggregate root
│   ├── device/      # Device aggregate root
│   ├── network/     # Network domain models
│   ├── security/    # Security domain models
│   ├── settings/    # Settings domain models
│   └── ports/       # Port definitions (traits)
│       ├── clipboard/
│       ├── security/
│       └── blob/
├── uc-infra/        # Infrastructure implementations (60% complete)
│   ├── db/          # Database layer
│   │   ├── mapper/  # Entity mappers
│   │   ├── models/  # Database models
│   │   └── repositories/ # Repository implementations
│   ├── security/    # Encryption implementations
│   └── settings/    # Settings storage
├── uc-platform/     # Platform adapter layer (70% complete)
│   ├── adapters/    # Platform-specific adapters
│   ├── app_runtime/ # Application runtime
│   ├── ipc/         # IPC event/command system
│   └── ports/       # Platform port definitions
└── uc-app/          # Application layer (30% complete)
    ├── event/       # Event handling
    ├── state/       # Application state
    └── use_cases/   # Use case implementations

Dependency flow: uc-appuc-coreuc-infra / uc-platform

Key architectural changes:

  • Port/Adapter pattern: All external dependencies accessed through trait ports
  • Message-driven runtime: Async event-based system replacing global state
  • Crate boundaries: Enforced separation through Rust module system

Legacy Architecture (Being Replaced)

The old architecture follows traditional Clean Architecture:

src-tauri/src/
├── domain/          # Core business models
├── interface/       # Trait definitions
├── infrastructure/  # External implementations
│   ├── clipboard/   # Platform-specific clipboard
│   ├── p2p/         # P2P network (libp2p)
│   ├── security/    # XChaCha20-Poly1305 encryption
│   ├── storage/     # Diesel ORM + SQLite
│   └── sync/        # WebSocket/WebDAV sync
├── application/     # Business services
├── config/          # TOML-based settings
├── api/             # Tauri command handlers
└── main.rs          # Application entry point

Status: Legacy code is still in use. Migration is in progress (~40% overall complete).

Concurrency patterns (legacy):

  • Tokio async runtime for I/O
  • Arc<Mutex<T>> for shared state
  • Global SETTING RwLock for configuration

Frontend (React 18 + TypeScript + Vite)

src/
├── pages/          # Route pages (Dashboard, Devices, Settings)
├── components/     # Reusable UI components (Shadcn/ui based)
├── layouts/        # Layout wrappers
├── store/          # Redux Toolkit slices (state management)
├── api/            # Tauri command invocations
├── contexts/       # React Context (SettingsProvider)
├── hooks/          # Custom React hooks
└── lib/            # Utilities (cn, shadcn UI helpers)

State management: Redux Toolkit with RTK Query Routing: React Router v7 UI: Tailwind CSS + Shadcn/ui components (Radix UI primitives)

Key Technical Details

Path Aliases

TypeScript path aliases configured: @/* maps to src/* (tsconfig.json:24-27)

Database Migrations

Diesel migrations in src-tauri/src/infrastructure/storage/db/migrations.rs. Run with diesel migration run (requires Diesel CLI setup).

Security Implementation

  • Encryption: XChaCha20-Poly1305 AEAD for clipboard content (src-tauri/crates/uc-infra/src/security/encryption.rs)
    • Chosen for its large nonce (192-bit) reducing nonce reuse risks
    • Provides authenticated encryption with associated data (AEAD)
    • Suitable for cross-platform applications with software-only implementation
  • Password hashing: Argon2 via Tauri Stronghold plugin
  • Key storage: Key slot file system with KEK-wrapped master keys (src-tauri/crates/uc-infra/src/fs/key_slot_store.rs)
  • Key derivation: Argon2id for passphrase-to-key derivation

Note: The aes-gcm dependency in Cargo.toml is currently unused and can be removed in a future cleanup.

Event System

  • Frontend listens to clipboard changes via listen_clipboard_new_content Tauri command
  • Backend publishes events through custom event bus
  • WebSocket events for cross-device sync

Platform-Specific Code

Configuration

Settings stored in TOML, managed by global SETTING RwLock (config/setting.rs). Includes:

  • General (silent_start, etc.)
  • Network (webserver_port)
  • Sync (websocket/webdav settings)
  • Security (encryption password)
  • Storage limits

Clipboard Capture Integration

Automatic Capture Flow

The application automatically captures clipboard content when it changes:

  1. ClipboardWatcher (Platform Layer) monitors system clipboard
  2. Sends PlatformEvent::ClipboardChanged { snapshot } when change detected
  3. PlatformRuntime receives event and calls ClipboardChangeHandler callback
  4. AppRuntime implements the callback, invokes CaptureClipboardUseCase
  5. UseCase persists event, representations, and creates ClipboardEntry

Important: Callback Architecture

The integration uses a callback pattern maintaining proper layer separation:

  • Platform Layer → depends on ClipboardChangeHandler trait (in uc-core/ports)
  • App Layer → implements ClipboardChangeHandler trait
  • Platform pushes changes upward via trait call
  • No dependency from Platform to App (follows DIP)

When Modifying

  • Platform Layer: Never call App layer directly, use callback trait
  • App Layer: Implement callback to handle events, can call multiple use cases
  • UseCase: execute_with_snapshot() for automatic capture, execute() for manual

Tauri Commands

All frontend-backend communication through Tauri commands defined in commands/ (new architecture) and api/ (legacy).

Current Commands (Hexagonal Architecture)

Clipboard Commands:

  • get_clipboard_entries - List clipboard history entries (uses ListClipboardEntries use case)
  • delete_clipboard_entry - Delete a clipboard entry (uses DeleteClipboardEntry use case)
  • capture_clipboard - Manually capture clipboard content (uses CaptureClipboard use case)

Encryption Commands:

  • initialize_encryption - Initialize encryption with passphrase (uses InitializeEncryption use case)
  • is_encryption_initialized - Check encryption initialization status (uses IsEncryptionInitialized use case)

Settings Commands (⚠️ Legacy - needs migration):

  • get_settings - Get application settings (direct Port access)
  • update_settings - Update application settings (direct Port access)

Architecture Pattern

Commands MUST follow the UseCases accessor pattern:

#[tauri::command]
pub async fn example_command(
    runtime: State<'_, AppRuntime>,
) -> Result<(), String> {
    let uc = runtime.usecases().example_use_case();
    uc.execute().await.map_err(|e| e.to_string())
}

Status: See docs/architecture/commands-status.md for detailed migration status.

Commands Layer Status

Current Migration Status: 5/7 commands using UseCases accessor (71%)

When adding new commands:

  1. Define command function in src-tauri/crates/uc-tauri/src/commands/
  2. Create/refer to use case in uc-app/src/usecases/
  3. Add accessor method to UseCases in src-tauri/crates/uc-tauri/src/bootstrap/runtime.rs
  4. Register in invoke_handler![] in src-tauri/src/main.rs
  5. Use runtime.usecases().xxx() - NEVER runtime.deps.xxx

See docs/architecture/commands-status.md for detailed status.

Development Notes

  • Package manager: Bun (not npm/yarn) - faster install/dev times
  • Dev server port: 1420 (configured in tauri.conf.json:8)
  • Release optimization: Size-optimized Rust profile (LTO, panic=abort, strip symbols) (Cargo.toml:87-92)
  • Single instance: Enforced via tauri-plugin-single-instance
  • Autostart: Managed via tauri-plugin-autostart (MacOS LaunchAgent on macOS)

Development Style

Problem-Solving Philosophy

CRITICAL: Don't treat symptoms in isolation. Always step back and analyze problems from a higher-level perspective before implementing fixes.

Symptoms vs. Root Causes:

❌ ANTI-PATTERN - Symptom-focused
"Component renders wrong" → Add useEffect hack → "State desync" → Add more hacks → Spaghetti code

✅ CORRECT - Root cause analysis
"Component renders wrong" → Trace data flow → Identify architectural gap → Design proper solution → Fix at the right layer

High-Level Thinking Checklist:

Before making changes, ask:

  1. Where does this problem originate?

    • UI layer issue, or state management problem?
    • API contract mismatch, or business logic gap?
    • Infrastructure limitation, or architectural flaw?
  2. What's the systemic fix?

    • Can this be solved by improving the abstraction?
    • Would a design pattern eliminate this class of bugs?
    • Is there a missing piece in the architecture?
  3. What are the trade-offs?

    • Short-term hack vs. long-term maintainability
    • Local fix vs. systemic improvement
    • Quick workaround vs. proper solution

Examples:

// ❌ WRONG - Treating symptoms everywhere
async fn sync_clipboard() {
    match send_to_device().await {
        Err(_) => sleep(Duration::from_secs(1)).await, // Band-aid
        Ok(_) => {}
    }
}

// ✅ CORRECT - Fix the retry logic at the infrastructure layer
// infrastructure/sync/retry_policy.rs
pub struct RetryPolicy {
    max_attempts: u32,
    backoff_strategy: BackoffStrategy,
}

async fn sync_clipboard_with_retry(policy: &RetryPolicy) -> Result<()> {
    policy.execute(|| send_to_device()).await
}
// ❌ WRONG - Local state patch
function DeviceList() {
  const [devices, setDevices] = useState([])
  useEffect(() => {
    fetchDevices().then(setDevices)
    setInterval(() => fetchDevices().then(setDevices), 5000) // Manual polling
  }, [])
}

// ✅ CORRECT - Leverage existing state management (Redux RTK Query)
function DeviceList() {
  const { data: devices } = useGetDevicesQuery() // Built-in caching, refetch, error handling
}

Rationale: High-level problem-solving prevents technical debt, reduces code complexity, and creates more maintainable solutions. Always identify the root cause and fix it at the appropriate abstraction layer.

Rust Error Handling

CRITICAL: Never use unwrap() or expect() in production code. Always handle errors explicitly:

// ❌ FORBIDDEN
let value = some_option.unwrap();
let result = some_result.expect("failed");

// ✅ CORRECT - Use pattern matching
match some_option {
    Some(value) => { /* handle value */ },
    None => { /* handle error case */ },
}

// ✅ CORRECT - Use ? operator with proper error propagation
pub fn do_something() -> Result<(), MyError> {
    let value = some_option.ok_or(MyError::NotFound)?;
    // ...
}

// ✅ CORRECT - Use unwrap_or/unwrap_or_default for non-critical defaults
let value = some_option.unwrap_or_default();
let config = config_option.unwrap_or_else(|| Config::default());

// ✅ ACCEPTABLE in tests only
#[cfg(test)]
mod tests {
    #[test]
    fn test_something() {
        let value = some_option.unwrap(); // OK in tests
    }
}

Rationale: Explicit error handling prevents panics in production, provides better error messages, and makes failure modes visible to callers.

Avoid Silent Failures in Event-Driven Code

CRITICAL: When handling events or commands in async/event-driven systems, never silently ignore errors. Always log errors and emit failure events when appropriate.

Anti-Pattern: Silent failures with if let Ok(...):

// ❌ WRONG - Silent failure, caller never knows the operation failed
NetworkCommand::SendPairingRequest { peer_id, message } => {
    if let Ok(peer) = peer_id.parse::<PeerId>() {
        self.swarm.send_request(&peer, request);
        debug!("Sent pairing request to {}", peer_id);
    }
    // If parsing fails, execution silently continues - user has no feedback!
}

Correct Pattern: Explicit error handling with logging and event emission:

// ✅ CORRECT - Log error and emit event for frontend to handle
NetworkCommand::SendPairingRequest { peer_id, message } => {
    match peer_id.parse::<PeerId>() {
        Ok(peer) => {
            self.swarm.send_request(&peer, request);
            debug!("Sent pairing request to {}", peer_id);
        }
        Err(e) => {
            warn!("Invalid peer_id '{}': {}", peer_id, e);
            let _ = self
                .event_tx
                .send(NetworkEvent::Error(format!(
                    "Failed to send pairing request: invalid peer_id '{}': {}",
                    peer_id, e
                )))
                .await;
        }
    }
}

Key Rules:

  1. Use match instead of if let - When the Err case represents a failure that users should know about
  2. Always log errors - Use warn!() or error!() to ensure failures are visible in logs
  3. Emit error events - Send NetworkEvent::Error or equivalent so the UI can display user-friendly error messages
  4. Handle missing resources - When an expected resource (like a pending channel) is missing, log a warning

When to use if let vs match:

// ✅ OK - Using if let when the None/Err case is truly benign
if let Some(value) = optional_cache.get(&key) {
    // Use cached value
}

// ✅ OK - Using if let when fallback behavior is acceptable
if let Ok(config) = read_config() {
    apply_config(config);
} else {
    use_default_config(); // Explicit fallback
}

// ❌ WRONG - Using if let when failure should be reported
if let Ok(peer_id) = str.parse::<PeerId>() {
    send_request(peer_id);
}
// Error is swallowed!

Tauri State Management

CRITICAL: All state accessed via tauri::State<'_, T> in commands MUST be registered with .manage() before the app starts.

Common Error: state not managed for field 'X' on command 'Y'. You must call .manage() before using this command

Root Cause: When a Tauri command uses state: tauri::State<'_, MyType> to access shared state, MyType must be registered in the Builder setup using .manage().

Correct Pattern:

// ❌ WRONG - AppRuntimeHandle created internally, never managed
// main.rs
fn run_app(setting: Setting) {
    Builder::default()
        .setup(|app| {
            // AppRuntime creates its own channels internally
            let runtime = AppRuntime::new(...).await?;
            // No .manage() call - commands will fail!
            Ok(())
        })
}

// api/clipboard_items.rs
#[tauri::command]
pub async fn get_clipboard_items(
    state: tauri::State<'_, AppRuntimeHandle>, // ERROR: not managed!
) -> Result<Vec<Item>, String> {
    // ...
}

// ✅ CORRECT - Create channels before setup, manage the handle
// main.rs
fn run_app(setting: Setting) {
    // Create channels FIRST
    let (clipboard_cmd_tx, clipboard_cmd_rx) = mpsc::channel(100);
    let (p2p_cmd_tx, p2p_cmd_rx) = mpsc::channel(100);

    // Create handle with senders
    let handle = AppRuntimeHandle::new(clipboard_cmd_tx, p2p_cmd_tx, Arc::new(setting));

    Builder::default()
        .manage(handle)  // Register BEFORE setup
        .setup(move |app| {
            // Pass receivers to runtime
            AppRuntime::new_with_channels(..., clipboard_cmd_rx, p2p_cmd_rx).await
        })
}

Key Rules:

  1. Create channels before Builder - Senders and receivers must be created outside .setup()
  2. Register with .manage() - Any type accessed via tauri::State must be managed
  3. Clone senders, move receivers - Senders can be cloned for the handle, receivers move to the runtime
  4. Use Arc for shared immutable data - Config and other read-only data should use Arc<T>

Rationale: Tauri's state system requires explicit registration to ensure thread safety and proper lifetime management. Commands can only access state that was registered before the app started.

Frontend Styling (Tailwind CSS)

CRITICAL: Avoid fixed pixel values (w-[XXpx], h-[XXpx]) for cross-platform compatibility. Use Tailwind's built-in utilities or relative units (rem) instead:

// ❌ FORBIDDEN - Fixed pixels don't scale across platforms/DPI
<div className="w-[200px] h-[60px]" />
<div className="min-w-[80px]" />
<div className="h-[1px]" />

// ✅ CORRECT - Use Tailwind utilities (rem-based)
<div className="w-52 h-15" />           // w-52 = 13rem, h-15 = 3.75rem
<div className="min-w-20" />            // min-w-20 = 5rem
<div className="h-px" />                // 1px height (special case)

// ✅ CORRECT - Use rem values directly when needed
<div className="w-[3.75rem]" />         // 60px = 3.75rem
<div className="h-[0.0625rem]" />       // 1px = 0.0625rem

// ✅ ACCEPTABLE - For truly fixed sizes (borders, shadows, etc.)
<div className="border shadow-lg" />

Rationale: Rem-based units scale with the root font size, providing better cross-platform consistency across different screen densities, DPI settings, and user accessibility preferences. Tailwind's default configuration uses 1rem = 16px.

Common Tailwind Width Reference:

  • w-16 = 4rem (64px)
  • w-20 = 5rem (80px)
  • w-52 = 13rem (208px)
  • h-px = 1px (special utility)

Testing

No test framework currently configured. When adding tests:

  • Rust tests go in src-tauri/tests/ or inline #[cfg(test)] modules
  • Frontend tests use Vitest (add to devDependencies)
  • Integration tests can use Cargo features: integration_tests, network_tests, hardware_tests

UI/UX Guidelines

Theme Support Best Practices

ALWAYS test components in both light and dark themes to ensure proper contrast and visibility.

Container Components (Dialog, Card, Popover, etc.):

  • Use bg-card + text-card-foreground for containers with content
  • Use bg-background only for page/base backgrounds
  • Use bg-muted for disabled/readonly states with text-foreground (not text-muted-foreground)

Common Pitfalls:

// ❌ WRONG - Background color on containers makes them blend in
<DialogContent className="bg-background" />

// ✅ CORRECT - Card color creates proper visual hierarchy
<DialogContent className="bg-card text-card-foreground" />

// ❌ WRONG - Muted text on readonly inputs is hard to read
<input className="bg-muted text-muted-foreground" readOnly />

// ✅ CORRECT - Muted background with foreground text
<input className="bg-muted/50 text-foreground" readOnly />

Status Messages:

  • Add border border-{color}/20 to banners for better visibility in light mode
  • Use font-medium on text for better readability
  • Ensure hover states use /70 opacity (not /60) for visibility