开源一个 macOS 剪贴板「意图识别」工具——不只是存储,更是理解你复制了什么

缘起

掘友们好,我是小牛不牛。

作为一个每天都在 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 ⭐。这比什么都实在。

相关推荐
ofoxcoding1 小时前
Codex Computer Use 完整指南:AI 自动操控 Mac 与 Windows 桌面实战详解
人工智能·windows·macos·ai
sg_knight5 小时前
Claude Code 安装指南(macOS / Linux / Windows WSL)
linux·windows·macos·claude·code·anthropic·claude-code
2601_9615934214 小时前
Rust 开发环境配置繁琐?RustRover 开箱即用搞定编码调试
开发语言·后端·macos·rust
在水一缸16 小时前
拒绝被打扰:深度解析 macOS 进程守护与“反自动启动”攻防战
macos·策略模式·用户体验·进程守护·自动启动·系统权限
REDcker20 小时前
macOS 挂载 Linux 远程目录详解
linux·运维·macos
2601_9615934220 小时前
Mac 上搭建Linux环境吗?VMware + CentOS Stream 9 镜像快速部署
linux·运维·ide·macos·centos
陪我去看海1 天前
受够了 AI 写完代码留下一堆端口?我做了一个端口管理App!
前端·macos·vibecoding
发光的沙子1 天前
FPGA----配置根文件系统ip和mac地址
网络协议·tcp/ip·macos