缘起
掘友们好,我是小牛不牛。
作为一个每天都在 Mac 上搬砖的程序员,我对「复制粘贴」这个操作有种近乎偏执的不满。
举个例子:我从 GitHub 复制一个链接,下一步想做的不是「再按 Cmd+V」,而是「打开浏览器」。但从剪贴板到浏览器,中间至少 4 个步骤。
有没有办法让剪贴板理解我复制了什么,然后直接帮我完成下一步?
于是有了 PurePaste。一个 MIT 开源的 macOS 菜单栏剪贴板工具。
先看效果:


核心能力
PasteFlow 意图识别
不同于传统剪贴板历史工具只做「存储 + 浏览」,PurePaste 的核心是识别内容类型。复制 URL → 弹出「浏览器打开」,复制颜色 → 展示色块预览。
技术实现上,每个类型是独立的检测函数,按优先级链排序(精确度高的先匹配):
swift
// 检测优先级(主干代码)
detectColor → #RGB / rgb()
detectIP → IPv4
detectEmail → x@x.x
detectURL → http(s) / www
detectPhone → 1xx xxxx xxxx
detectTracking → SF/YT/EMS...
detectMath → 算术表达式(递归下降解析)
detectGeo → 经纬度坐标
detectDatetime → 日期时间格式
detectAddress → 省/市/街道关键词
每种检测函数返回枚举值,携带解析后的结构化数据。例如:
swift
enum DetectedContent {
case url(URL)
case email(String)
case color(NSColor, String) // (色彩对象, HEX字符串)
case mathExpression(String, Double) // (原式, 计算结果)
case geoCoordinate(Double, Double) // (纬度, 经度)
case richHTML(String, Data) // (纯文本, HTML源数据)
}
浮窗操作通过 PasteFlowAction 枚举分派,ActionExecutor 执行。完整代码在 ContentDetector.swift。
手写递归下降数学解析器
为什么不直接用 NSExpression?
NSExpression(format:) 中 % 是格式字符串占位符。NSExpression(format: "10%3") 会直接抛异常。于是我手写了一个标准的递归下降解析器。
文法设计
javascript
Expr → Term (("+"|"-") Term)*
Term → Power (("*"|"/"|"%") Power)*
Power → Unary ("^" Unary)?
Unary → ("+"|"-")? Atom
Atom → Number | "(" Expr ")"
源码在 evaluateMath() 函数中,含完整 tokenizer + 6 个递归函数,约 150 行。
踩坑:off-by-one
tokenizer 末尾有一个 .end 哨兵 token。tokens.count 包含了 .end,解析正确停止在倒数第二个位置。校验条件应该是:
swift
pos == tokens.count - 1 // 正确:解析停在 .end 处
pos == tokens.count // 错误:永远对不上,所有数学检测返回 nil
这个 bug 导致所有数学表达式被识别为「富文本」。修复后,(35+47)*1.2 → 数学计算 ✅。
浮动面板的 NSPanel 技术
Mac 开发中,弹出「不抢焦点」的面板是很常见的需求。PurePaste 的做法:
swift
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 280, height: 200),
styleMask: [.borderless, .nonactivatingPanel], // 关键
backing: .buffered, defer: false
)
window.level = .floating // 始终在最上层
window.collectionBehavior = [.canJoinAllSpaces, .ignoresCycle]
window.hasShadow = true
// SwiftUI 嵌入
window.contentView = NSHostingView(rootView: FloatingPanelView(...))
// 定位到鼠标
let mousePos = NSEvent.mouseLocation
window.setFrame(CGRect(x: mousePos.x + 16, y: mousePos.y - height, ...), display: true)
ESC 关闭通过 NSEvent.addLocalMonitorForEvents(.keyDown) 实现,外部点击关闭通过 addGlobalMonitorForEvents(.leftMouseDown) 实现。
剪贴板死循环防护
剪贴板工具最经典的 bug 是「写回→触发监听→再读→再写」的死循环。我的方案:
swift
// 方案 1:写入前置标志位
internalWriteFlag = true // 顶部 guard 会拦截
pasteboard.clearContents()
pasteboard.setString(processed, forType: .string)
// 方案 2:写入后立即更新计数器
lastChangeCount = pasteboard.changeCount // 下次 check 发现没变化 → 跳过
internalWriteFlag = false
在纯文本模式下(需要写回剪贴板)使用双层防护,PasteFlow 模式下(只读不写)只更新 lastChangeCount。
意图历史的数据模型
这或许是 PurePaste 最创新的设计:
swift
struct HistoryEntry: Codable, Identifiable {
let id: UUID
let text: String
let mode: String // plainText | pasteFlow
let detectedType: String? // URL | 邮箱 | 颜色值 | ...
let action: String? // 浏览器打开 | 复制 HEX | ...
let isIntentFulfilled: Bool
}
传统剪贴板历史只存 text + timestamp。PurePaste 每条记录带 5 个维度,支持按类型 / 模式 / 意图状态 / 关键词多维筛选。
存储:~/Library/Application Support/PurePaste/history.json。内存常驻,JSON 直接过滤,不引入数据库。
中英双语实现
没有用 .strings 文件。所有文案集中在 L10n 枚举中,根据 preferredLanguage 返回对应文本。支持三态:auto(跟随系统)/ zh(强制中文)/ en(强制英文)。
swift
enum L10n {
static var isChinese: Bool {
switch Language(rawValue: preferredLanguage) ?? .auto {
case .chinese: return true
case .english: return false
case .auto: return systemIsChinese
}
}
}
实际效果图
颜色值识别: 
数学计算: 
意图历史窗口: 
编译与使用
bash
git clone https://github.com/xiaoyunchengzhu/PurePaste.git
cd PurePaste
open PurePaste.xcodeproj
# Cmd+R 运行
macOS 14.0+ · Xcode 15.0+ · Swift 5.0
产品首页(含 DMG 下载):xiaoniubuniu.com/products/pu...
总结
PurePaste 是一个练手向 + 实用向的开源项目。技术栈覆盖了 macOS 开发的多个常见场景:剪贴板操作、自定义窗口样式、递归解析器、数据持久化、多语言、Timer 轮询。
如果你也在学习 macOS 开发,欢迎 clone 下来看源码、提 Issue、贡献 PR。
如果觉得有用,给个 Star ⭐。这比什么都实在。