| name | tiez-clipboard-manager |
| description | TieZ is a Tauri-based cross-platform clipboard manager with history, tags, sync (WebDAV/MQTT), privacy protection, and fast workflows for managing clipboard data across devices. |
| triggers | ["how do I set up TieZ clipboard manager","configure TieZ clipboard sync with WebDAV","build TieZ clipboard from source","develop features for TieZ clipboard manager","customize TieZ clipboard themes","integrate MQTT sync in TieZ","troubleshoot TieZ clipboard build errors","add tauri commands to TieZ clipboard"] |
TieZ Clipboard Manager Skill
Skill by ara.so — Devtools Skills collection.
Overview
TieZ is a cross-platform clipboard manager built with Tauri 2, React, and Rust. It provides clipboard history tracking, tag organization, privacy masking, cloud sync (WebDAV/MQTT), and multiple UI themes. The application uses a native Rust backend for performance and a TypeScript/React frontend for the UI.
Key Technologies:
- Tauri 2: Desktop framework (Rust backend + web frontend)
- React: Frontend UI framework
- TypeScript: Frontend language
- Rust: Backend logic, clipboard monitoring, file operations
- WebDAV/MQTT: Sync protocols for cross-device data
Installation & Setup
Prerequisites
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
brew install node
npm install -g pnpm
Clone and Build
git clone https://github.com/jimuzhe/tiez-clipboard.git
cd tiez-clipboard
pnpm install
pnpm tauri dev
pnpm tauri build
Development Structure
tiez-clipboard/
├── src/ # React frontend (TypeScript)
├── src-tauri/ # Rust backend
│ ├── src/
│ │ ├── main.rs
│ │ ├── clipboard/
│ │ ├── sync/
│ │ └── commands/
│ ├── Cargo.toml
│ └── tauri.conf.json
├── package.json
└── vite.config.ts
Key Concepts
Tauri Commands
TieZ uses Tauri commands to bridge Rust backend and React frontend:
Backend (Rust):
use tauri::State;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct ClipboardItem {
pub id: String,
pub content: String,
pub content_type: String,
pub timestamp: i64,
pub tags: Vec<String>,
}
#[tauri::command]
pub async fn get_clipboard_history(
limit: Option<usize>,
) -> Result<Vec<ClipboardItem>, String> {
let items = query_history(limit.unwrap_or(100))
.await
.map_err(|e| e.to_string())?;
Ok(items)
}
#[tauri::command]
pub async fn add_clipboard_item(
content: String,
content_type: String,
tags: Vec<String>,
) -> Result<ClipboardItem, > {
= (content, content_type, tags)
.
.(|e| e.())?;
(item)
}
(id: ) <(), > {
(&id)..(|e| e.())?;
(())
}
Frontend (TypeScript/React):
import { invoke } from '@tauri-apps/api/core';
export interface ClipboardItem {
id: string;
content: string;
content_type: string;
timestamp: number;
tags: string[];
}
export async function getClipboardHistory(
limit?: number
): Promise<ClipboardItem[]> {
return await invoke<ClipboardItem[]>('get_clipboard_history', { limit });
}
export async function addClipboardItem(
content: string,
contentType: string,
tags: string[]
): Promise<ClipboardItem> {
return await invoke<ClipboardItem>('add_clipboard_item', {
content,
contentType,
tags,
});
}
export async function deleteClipboardItem(: ): <> {
(, { id });
}
Clipboard Monitoring
use clipboard_master::{CallbackResult, ClipboardHandler, Master};
use std::sync::{Arc, Mutex};
use tauri::{AppHandle, Manager};
pub struct ClipboardMonitor {
app_handle: AppHandle,
}
impl ClipboardHandler for ClipboardMonitor {
fn on_clipboard_change(&mut self) -> CallbackResult {
if let Ok(content) = get_clipboard_text() {
self.app_handle
.emit_all("clipboard-changed", content)
.unwrap_or_default();
}
CallbackResult::Next
}
}
pub fn start_monitoring(app_handle: AppHandle) {
std::thread::spawn(move || {
let monitor = ClipboardMonitor { app_handle };
Master::new(monitor).run().unwrap();
});
}
WebDAV Sync Integration
use reqwest::Client;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct WebDAVConfig {
pub url: String,
pub username: String,
pub password: String,
pub path: String,
}
pub struct WebDAVSync {
client: Client,
config: WebDAVConfig,
}
impl WebDAVSync {
pub fn new(config: WebDAVConfig) -> Self {
Self {
client: Client::new(),
config,
}
}
pub async fn upload_data(&self, data: &str) -> Result<(), String> {
let url = format!("{}/{}", self.config.url, self.config.path);
self.client
.put(&url)
.basic_auth(&self.config.username, Some(&self.config.password))
.(data.())
.()
.
.(|e| e.())?;
(())
}
(&) <, > {
= (, .config.url, .config.path);
= .client
.(&url)
.(&.config.username, (&.config.password))
.()
.
.(|e| e.())?;
response.()..(|e| e.())
}
}
(
config: WebDAVConfig,
data: ,
) <(), > {
= WebDAVSync::(config);
sync.(&data).
}
MQTT Real-time Sync
use rumqttc::{AsyncClient, MqttOptions, QoS};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct MQTTConfig {
pub broker: String,
pub port: u16,
pub client_id: String,
pub username: Option<String>,
pub password: Option<String>,
pub topic: String,
}
pub async fn publish_clipboard_item(
config: &MQTTConfig,
item: &ClipboardItem,
) -> Result<(), String> {
let mut mqttoptions = MqttOptions::new(
&config.client_id,
&config.broker,
config.port,
);
if let (Some(username), Some(password)) =
(&config.username, &config.password) {
mqttoptions.set_credentials(username, password);
}
let (client, mut eventloop) = AsyncClient::new(mqttoptions, 10);
let payload = serde_json::to_string(item)
.map_err(|e| e.to_string())?;
client
.(&config.topic, QoS::AtLeastOnce, , payload)
.
.(|e| e.())?;
(())
}
(
config: MQTTConfig,
item: ClipboardItem,
) <(), > {
(&config, &item).
}
Frontend Development
React Component Example
import React, { useEffect, useState } from 'react';
import { listen } from '@tauri-apps/api/event';
import { getClipboardHistory, ClipboardItem } from '../api/clipboard';
export const ClipboardHistory: React.FC = () => {
const [items, setItems] = useState<ClipboardItem[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadHistory();
const unlisten = listen<string>('clipboard-changed', (event) => {
console.log('Clipboard changed:', event.payload);
loadHistory();
});
return () => {
unlisten.then((fn) => fn());
};
}, []);
const loadHistory = async () => {
setLoading();
{
history = ();
(history);
} (error) {
.(, error);
} {
();
}
};
(
);
};
Settings Management
import { invoke } from '@tauri-apps/api/core';
export interface AppSettings {
theme: 'frosted' | 'notebook' | 'sticky' | '3d';
darkMode: boolean;
autoStart: boolean;
maxHistoryItems: number;
syncEnabled: boolean;
webdavConfig?: {
url: string;
username: string;
path: string;
};
mqttConfig?: {
broker: string;
port: number;
clientId: string;
topic: string;
};
}
export async function getSettings(): Promise<AppSettings> {
return await invoke<AppSettings>('get_settings');
}
export async function updateSettings(
settings: Partial<>
): <> {
(, { settings });
}
use tauri::State;
use serde::{Deserialize, Serialize};
use std::sync::Mutex;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppSettings {
pub theme: String,
pub dark_mode: bool,
pub auto_start: bool,
pub max_history_items: usize,
pub sync_enabled: bool,
}
pub struct SettingsState(pub Mutex<AppSettings>);
#[tauri::command]
pub fn get_settings(state: State<SettingsState>) -> Result<AppSettings, String> {
let settings = state.0.lock().map_err(|e| e.to_string())?;
Ok(settings.clone())
}
#[tauri::command]
pub fn update_settings(
settings: AppSettings,
state: State<SettingsState>,
) -> Result<(), String> {
let mut current = state.0.lock().(|e| e.())?;
*current = settings;
(¤t)?;
(())
}
Configuration
Tauri Configuration
{
"build": {
"beforeDevCommand": "pnpm dev",
"beforeBuildCommand": "pnpm build",
"devPath": "http://localhost:5173",
"distDir": "../dist"
},
"package": {
"productName": "TieZ",
"version": "1.0.0"
},
"tauri": {
"allowlist": {
"all": false,
"clipboard": {
"all": true
},
"fs": {
"all"
Environment Variables
VITE_APP_NAME=TieZ
VITE_DEFAULT_WEBDAV_URL=https://dav.example.com
VITE_DEFAULT_MQTT_BROKER=mqtt.example.com
VITE_DEFAULT_MQTT_PORT=1883
Common Development Patterns
Adding a New Tauri Command
- Define command in Rust:
#[tauri::command]
pub async fn my_custom_command(param: String) -> Result<String, String> {
Ok(format!("Processed: {}", param))
}
- Register in main.rs:
mod commands;
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
commands::clipboard::get_clipboard_history,
commands::custom::my_custom_command,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
- Call from frontend:
import { invoke } from '@tauri-apps/api/core';
export async function myCustomCommand(param: string): Promise<string> {
return await invoke<string>('my_custom_command', { param });
}
Creating a New Theme
export const customTheme = {
name: 'custom',
colors: {
primary: '#4CAF50',
secondary: '#2196F3',
background: '#FFFFFF',
surface: '#F5F5F5',
text: '#212121',
},
effects: {
blur: 10,
transparency: 0.9,
},
};
.theme-custom {
--color-primary: #4caf50;
--color-secondary: #2196f3;
--color-background: #ffffff;
--color-surface: #f5f5f5;
--color-text: #212121;
backdrop-filter: blur(10px);
background: rgba(255, 255, 255, 0.9);
}
Privacy Masking Implementation
use regex::Regex;
pub fn mask_sensitive_data(content: &str) -> String {
let mut masked = content.to_string();
let phone_regex = Regex::new(r"\b\d{3}-\d{3,4}-\d{4}\b").unwrap();
masked = phone_regex.replace_all(&masked, "***-****-****").to_string();
let email_regex = Regex::new(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b").unwrap();
masked = email_regex.replace_all(&masked, "***@***.***").to_string();
let cc_regex = Regex::new(r"\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b").unwrap();
masked = cc_regex.replace_all(&masked, "**** **** **** ****").to_string();
masked
}
#[tauri::command]
pub fn get_masked_preview(content: String) {
(&content)
}
Troubleshooting
Build Errors
Issue: Tauri build fails with "command not found"
source $HOME/.cargo/env
rustup update
cargo clean
pnpm tauri build
Issue: Frontend build fails
rm -rf node_modules pnpm-lock.yaml
pnpm install
rm -rf node_modules/.vite
pnpm dev
Runtime Issues
Issue: Clipboard monitoring not working
"allowlist": {
"clipboard": {
"all": true,
"readText": true,
"writeText": true
}
}
Issue: WebDAV sync fails with CORS error
- WebDAV requests are made from Rust backend (no CORS)
- Ensure
http.all: true in tauri.conf.json allowlist
- Check network connectivity and credentials
Issue: App doesn't start on system boot
use tauri_plugin_autostart::MacosLauncher;
tauri::Builder::default()
.plugin(tauri_plugin_autostart::init(
MacosLauncher::LaunchAgent,
Some(vec!["--flag1", "--flag2"])
))
Development Tips
cargo watch -x 'run --features tauri/dev'
RUST_LOG=debug pnpm tauri dev
pnpm tauri build --verbose
pnpm tauri build -- --bundles deb,app,msi,dmg
Database Migration
use rusqlite::{Connection, Result};
pub fn migrate_database(conn: &Connection) -> Result<()> {
conn.execute(
"CREATE TABLE IF NOT EXISTS clipboard_items (
id TEXT PRIMARY KEY,
content TEXT NOT NULL,
content_type TEXT NOT NULL,
timestamp INTEGER NOT NULL,
tags TEXT
)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_timestamp
ON clipboard_items(timestamp DESC)",
[],
)?;
Ok(())
}
Testing
import { describe, it, expect, beforeAll } from 'vitest';
import { mockIPC } from '@tauri-apps/api/mocks';
import { getClipboardHistory } from '../api/clipboard';
describe('Clipboard API', () => {
beforeAll(() => {
mockIPC((cmd, args) => {
if (cmd === 'get_clipboard_history') {
return [
{
id: '1',
content: 'test',
content_type: 'text',
timestamp: Date.now(),
tags: [],
},
];
}
});
});
it('should fetch clipboard history', async () => {
const history = await getClipboardHistory();
expect(history).toHaveLength(1);
expect(history[0].content).toBe('test');
});
});
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_phone_masking() {
let content = "Call me at 123-456-7890";
let masked = mask_sensitive_data(content);
assert_eq!(masked, "Call me at ***-****-****");
}
#[test]
fn test_email_masking() {
let content = "Email: user@example.com";
let masked = mask_sensitive_data(content);
assert_eq!(masked, "Email: ***@***.***");
}
}