Rust实现Windows 系统剪贴板历史工具
·
一、项目概述
本项目实现一个 Windows 平台剪贴板历史工具。
程序在后台运行,自动监听系统剪贴板变化(复制文本/图片路径),并将历史内容保存到环形缓冲区。
用户可以通过快捷键 Ctrl + Shift + V 打开终端界面(TUI)浏览、搜索并一键粘贴。
二、项目功能
| 功能 | 说明 |
|---|---|
| 实时监听 | 监控剪贴板变化(文本/图片路径) |
| 自动保存 | 历史保存在环形缓冲区 |
| 快捷键呼出 | Ctrl + Shift + V 打开终端界面 |
| TUI 浏览 | 可滚动查看历史,支持自动刷新 |
| 一键粘贴 | 选中项直接粘贴到当前焦点窗口 |
| 后台运行 | 长期监听,资源占用极低 |
三、项目结构
cliphist/
├── Cargo.toml
└── src/
├── main.rs
├── storage.rs
├── platform/
│ ├── mod.rs
│ └── windows.rs
├── paste.rs
├── tui_app.rs
└── utils.rs
四、Cargo.toml
[package]
name = "cliphist"
version = "0.1.0"
edition = "2021"
[dependencies]
winapi = { version = "0.3", features = ["winuser", "windowsx", "wingdi", "shellapi", "libloaderapi", "winbase"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
dirs = "4.0"
tui = "0.19"
crossterm = "0.26"
rdev = "0.5"
chrono = "0.4"
uuid = { version = "1", features = ["v4"] }
五、完整源码
src/main.rs
mod platform;
mod storage;
mod tui_app;
mod paste;
mod utils;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
use storage::RingBuffer;
fn main() {let store = Arc::new(Mutex::new(RingBuffer::new(200)));
// 启动剪贴板监听线程
platform::windows::start_clipboard_listener(store.clone());
// 注册全局热键 Ctrl + Shift + V 打开 TUI
platform::windows::register_hotkey(move || {
tui_app::show_tui(store.clone());
});
println!("📋 ClipHist running... (Ctrl+Shift+V to open panel)");
// 主线程挂起(后台常驻)
loop {
thread::sleep(Duration::from_secs(60));
}
}
src/storage.rs
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Clone)]
pub enum ClipContent {Text(String),Image(String),
}
#[derive(Serialize, Deserialize, Clone)]
pub struct ClipItem {pub id: String,pub timestamp: i64,pub content: ClipContent,
}
pub struct RingBuffer {
capacity: usize,
items: Vec<ClipItem>,
}
impl RingBuffer {pub fn new(capacity: usize) -> Self {Self { capacity, items: Vec::new() }
}
pub fn push(&mut self, item: ClipItem) {if self.items.len() >= self.capacity {self.items.remove(0);
}self.items.push(item);
}
pub fn all(&self) -> &Vec<ClipItem> {
&self.items
}
}
src/platform/mod.rs
#[cfg(target_os = "windows")]
pub mod windows;
src/platform/windows.rs
use std::sync::{Arc, Mutex};
use winapi::shared::windef::HWND;
use winapi::shared::minwindef::{LPARAM, LRESULT, UINT, WPARAM};
use winapi::um::winuser::*;
use winapi::um::winbase::{GlobalLock, GlobalUnlock, GlobalAlloc, GlobalFree, GMEM_MOVEABLE};
use winapi::um::libloaderapi::GetModuleHandleW;
use crate::storage::{ClipItem, ClipContent, RingBuffer};
use uuid::Uuid;
use chrono::Utc;
use std::ptr;
pub fn start_clipboard_listener(store: Arc<Mutex<RingBuffer>>) {
std::thread::spawn(move || unsafe {let h_instance = GetModuleHandleW(ptr::null());let class_name = wide_str("ClipHistWnd");
let wc = WNDCLASSW {
lpfnWndProc: Some(window_proc),
hInstance: h_instance,
lpszClassName: class_name.as_ptr(),
..std::mem::zeroed()
};
RegisterClassW(&wc);
let hwnd = CreateWindowExW(0,
class_name.as_ptr(),wide_str("ClipHist").as_ptr(),0,0, 0, 0, 0,
ptr::null_mut(),
ptr::null_mut(),
h_instance,
ptr::null_mut(),
);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, Box::into_raw(Box::new(store)) as isize);AddClipboardFormatListener(hwnd);
let mut msg = std::mem::zeroed();while GetMessageW(&mut msg, hwnd, 0, 0) > 0 {TranslateMessage(&msg);DispatchMessageW(&msg);
}
});
}
unsafe extern "system" fn window_proc(hwnd: HWND, msg: UINT, _wparam: WPARAM, _lparam: LPARAM) -> LRESULT {match msg {
WM_CLIPBOARDUPDATE => {let ptr = GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *mut Arc<Mutex<RingBuffer>>;if !ptr.is_null() {let store = &*ptr;if let Some(item) = read_clipboard() {
store.lock().unwrap().push(item);
}
}0
}
_ => DefWindowProcW(hwnd, msg, _wparam, _lparam),
}
}
unsafe fn read_clipboard() -> Option<ClipItem> {if OpenClipboard(ptr::null_mut()) == 0 {return None;
}
let handle = GetClipboardData(CF_UNICODETEXT);if handle.is_null() {CloseClipboard();return None;
}
let ptr_text = GlobalLock(handle) as *const u16;if ptr_text.is_null() {CloseClipboard();return None;
}
let text = String::from_utf16_lossy(&u16_ptr_to_vec(ptr_text));GlobalUnlock(handle);CloseClipboard();
Some(ClipItem {
id: Uuid::new_v4().to_string(),
timestamp: Utc::now().timestamp(),
content: ClipContent::Text(text),
})
}
unsafe fn u16_ptr_to_vec(ptr: *const u16) -> Vec<u16> {let mut v = Vec::new();let mut i = 0;while *ptr.add(i) != 0 {
v.push(*ptr.add(i));
i += 1;
}
v
}
fn wide_str(s: &str) -> Vec<u16> {use std::os::windows::ffi::OsStrExt;
std::ffi::OsStr::new(s).encode_wide().chain(Some(0)).collect()
}
pub fn set_clipboard_text(text: &str) {unsafe {if OpenClipboard(ptr::null_mut()) == 0 {return;
}EmptyClipboard();
let wide: Vec<u16> = text.encode_utf16().chain(std::iter::once(0)).collect();let size = wide.len() * std::mem::size_of::<u16>();
let h_mem = GlobalAlloc(GMEM_MOVEABLE, size);if h_mem.is_null() {CloseClipboard();return;
}
let ptr_mem = GlobalLock(h_mem) as *mut u16;if ptr_mem.is_null() {GlobalFree(h_mem);CloseClipboard();return;
}
std::ptr::copy_nonoverlapping(wide.as_ptr(), ptr_mem, wide.len());GlobalUnlock(h_mem);SetClipboardData(CF_UNICODETEXT, h_mem);CloseClipboard();
}
}
pub fn register_hotkey(callback: impl Fn() + Send + 'static) {
std::thread::spawn(move || unsafe {let hotkey_id: i32 = 1;RegisterHotKey(ptr::null_mut(), hotkey_id, (MOD_CONTROL | MOD_SHIFT) as u32, 0x56); // 'V'
let mut msg = std::mem::zeroed();while GetMessageW(&mut msg, ptr::null_mut(), 0, 0) > 0 {if msg.message == WM_HOTKEY {callback();
}
}
});
}
src/paste.rs
use crate::storage::ClipItem;
use rdev::{simulate, Key, SimulateError};
pub fn simulate_paste(item: &ClipItem) {if let crate::storage::ClipContent::Text(text) = &item.content {
crate::platform::windows::set_clipboard_text(text);let _ = paste_combo();
}
}
fn paste_combo() -> Result<(), SimulateError> {simulate(&rdev::EventType::KeyPress(Key::ControlLeft))?;simulate(&rdev::EventType::KeyPress(Key::KeyV))?;simulate(&rdev::EventType::KeyRelease(Key::KeyV))?;simulate(&rdev::EventType::KeyRelease(Key::ControlLeft))?;Ok(())
}
src/tui_app.rs
use crate::storage::{RingBuffer, ClipContent};
use crossterm::{
event::{self, Event, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType},
execute,
};
use tui::{
backend::CrosstermBackend,
Terminal,
widgets::{Block, Borders, List, ListItem},
layout::{Layout, Constraint, Direction},
};
use std::{
io::{self, stdout},
sync::{Arc, Mutex},
time::{Duration, Instant},
};
use crate::paste::simulate_paste;
pub fn show_tui(store: Arc<Mutex<RingBuffer>>) {
execute!(stdout(), Clear(ClearType::All)).unwrap();enable_raw_mode().unwrap();let mut stdout = io::stdout();let backend = CrosstermBackend::new(&mut stdout);let mut terminal = Terminal::new(backend).unwrap();
let mut index = 0;let mut last_draw = Instant::now();
loop {if last_draw.elapsed() >= Duration::from_millis(500) {let items = store.lock().unwrap().all().clone();
terminal
.draw(|f| {let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(100)].as_ref())
.split(f.size());
let list_items: Vec<ListItem> = items
.iter()
.rev()
.enumerate()
.map(|(i, it)| {let content = match &it.content {
ClipContent::Text(t) => t.clone(),
ClipContent::Image(p) => format!("[Image] {}", p),
};
ListItem::new(format!("{}: {}", i, content))
})
.collect();
let list = List::new(list_items)
.block(Block::default().title("📋 Clipboard History (Auto Refresh)").borders(Borders::ALL));
f.render_widget(list, chunks[0]);
})
.unwrap();
last_draw = Instant::now();
}
if event::poll(Duration::from_millis(100)).unwrap() {if let Event::Key(key) = event::read().unwrap() {match key.code {
KeyCode::Esc => break,
KeyCode::Up => if index > 0 { index -= 1; },
KeyCode::Down => index += 1,
KeyCode::Enter => {let items = store.lock().unwrap().all().clone();if let Some(item) = items.iter().rev().nth(index).cloned() {simulate_paste(&item);
}break;
}
_ => {}
}
}
}
}
disable_raw_mode().unwrap();
}
src/utils.rs
use std::path::PathBuf;
use dirs::data_local_dir;
pub fn app_data_dir() -> PathBuf {let mut dir = data_local_dir().unwrap_or(std::env::temp_dir());
dir.push("cliphist");
std::fs::create_dir_all(&dir).ok();
dir
}
六、运行
cargo run --quiet
运行后:
- 复制任意文字;
- 按 Ctrl + Shift + V;
- 面板实时显示剪贴板历史;

支持实时刷新,每个一段时间刷新面板

七、效果演示
Clipboard History (Auto Refresh)
───────────────────────────────────────────────
0: Hello, world!
1: cargo run --quiet
2: Rust clipboard test
───────────────────────────────────────────────
用 Rust 打造跨平台、高性能的系统剪贴板历史工具,让「复制不丢失、粘贴更自由」。
想了解更多关于Rust语言的知识及应用,可前往华为开放原子旋武开源社区(https://xuanwu.openatom.cn/),了解更多资讯~
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)