# clip-board
**Repository Path**: solidify-the-foundation/clip-board
## Basic Information
- **Project Name**: clip-board
- **Description**: mac的粘贴板历史
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: main
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-06-27
- **Last Updated**: 2026-06-27
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# ClipBoard
> 一个轻量、常驻状态栏的 macOS 剪贴板历史工具。Swift 原生实现,零第三方依赖。
   
---
## ✨ 功能一览
### 📋 剪贴板历史
- **多类型支持**:📝 文本 / 🖼️ 图片 / 📎 文件 / 📄 RTF 富文本
- **自动捕获**:DispatchSourceTimer 500ms 轮询,能耗几乎为 0
- **本地持久化**:JSON 防抖落盘,重启不丢历史
- **去重 + 容量裁剪**:SHA256 内容哈希,最旧条目自动淘汰
### 🎯 高效粘贴
- **全局快捷键**(默认 `⇧⌘V`):随处一按弹出浮窗
- **模糊背景浮窗**:非激活式 NSPanel,不打断当前 App 焦点
- **实时搜索**:输入即过滤;RTF 也能按内部纯文本搜
- **自动粘贴**:选中后写回剪贴板 + 模拟 ⌘V 一步到位
- **键盘全控制**:↑/↓ 选择、回车粘贴、`⌘1`~`⌘9` 快选、`⌘P` 收藏、`⌘⌫` 删除、`Esc` 关闭
### 🗂️ 整理与筛选
- **类型筛选 Tab**:全部 / 收藏 / 文本 / 图片 / 文件 / 富文本
- **收藏置顶**:置顶条目不会被容量淘汰
- **来源 App 显示**:每条历史显示从哪个 App 复制(含 App 图标)
- **图片真实缩略图**:LRU 缓存,列表滚动丝滑
### 🛡️ 隐私保护(三道防线)
1. **系统瞬态/敏感类型自动忽略**(`org.nspasteboard.{Transient,Concealed,AutoGenerated}Type`、1Password 等)
2. **用户自定义 App 黑名单**(按 Bundle ID)
3. **单条体积上限**(默认 8MB,防止巨图塞爆历史)
### ⚙️ 个性化
- **自定义全局快捷键**:录制任意带修饰键的组合
- **开机自启**(SMAppService)
- **容量配置**(20~2000 条)
- **单条最大体积配置**(1MB~32MB)
---
## 🚀 快速开始
> **要求**:macOS 13 Ventura+,Swift 5.9+(Command Line Tools 即可,无需完整 Xcode)
### 方式一:开发环境直接跑
```bash
swift run -c release ClipBoard
```
最快速度看到效果,但不支持开机自启。
### 方式二:打包成 .app(推荐)
```bash
./scripts/build-app.sh
open build/ClipBoard.app
# 或安装:
cp -R build/ClipBoard.app /Applications/
```
### 方式三:出 DMG 分发
```bash
./scripts/build-app.sh && ./scripts/make-dmg.sh
# 输出:build/ClipBoard-0.4.0.dmg(含 Applications 软链)
```
### 首次启动:授权辅助功能(用于自动粘贴)
1. 菜单栏图标 → 点击 "⚠️ 未授权辅助功能..." 项
2. 系统设置 → 隐私与安全 → 辅助功能 → 勾选 `ClipBoard`
3. 重启 ClipBoard
> 不授权也能用,只是选中条目只写剪贴板,需要你手动 ⌘V。
#### 已经授权了,菜单还显示"未授权"?
最常见的是 **TCC「幻觉授权」**:你之前授权过 `swift run` 跑的裸二进制,后来又装了 `.app`,两者在系统看是不同的 client。一行命令解决:
```bash
# 退出 ClipBoard,然后:
sudo tccutil reset Accessibility com.clipboard.history
# 重新启动,按提示再授权一次
open /Applications/ClipBoard.app
```
> 授权生效后**不需要重启 ClipBoard**,点一下菜单栏图标,"⚠️"提示会自动消失(菜单每次打开都会重新检测权限状态)。
---
## ⌨️ 快捷键
### 全局
| 快捷键 | 操作 |
|--------|------|
| `⇧⌘V`(可改) | 打开 / 关闭浮窗 |
### 浮窗内
| 快捷键 | 操作 |
|--------|------|
| `↑` / `↓` | 上下选择 |
| `↵` | 粘贴选中 |
| `⌘1` ~ `⌘9` | 粘贴第 N 条 |
| `⌘P` | 收藏 / 取消收藏 |
| `⌘⌫` | 删除选中 |
| `Esc` | 关闭浮窗 |
| 右键 | 收藏 / 粘贴 / 删除 菜单 |
### 状态栏菜单
| 快捷键 | 操作 |
|--------|------|
| `⌘,` | 打开设置 |
| `⌘Q` | 退出 |
---
## 📁 数据位置
```
~/Library/Application Support/ClipBoard/history.json # 剪贴板历史
```
偏好设置存储在 UserDefaults(domain: `com.clipboard.history`)。
删除上述文件 = 清空所有历史。
---
## 🏗️ 项目结构
```
clip-board/
├── PLAN.md / README.md
├── Package.swift # SPM manifest
├── Resources/Info.plist # .app bundle 元数据
├── scripts/
│ ├── build-app.sh # 打包成 .app
│ └── make-dmg.sh # 打包成 DMG
└── Sources/ClipBoard/
├── App/ # 入口
│ ├── main.swift
│ └── AppDelegate.swift # 装配所有模块
├── Core/
│ ├── Store/
│ │ ├── ClipItem.swift # 多类型 Codable 数据模型
│ │ ├── HistoryStore.swift # 线程安全 + 去重 + 裁剪
│ │ └── JSONPersistence.swift # 防抖落盘
│ ├── Shortcut/
│ │ ├── HotKey.swift # Carbon 全局快捷键
│ │ └── ShortcutManager.swift # 可配置快捷键
│ ├── Launch/LaunchAtLogin.swift # SMAppService
│ └── Util/Hashing.swift # SHA256
└── Features/
├── MenuBar/MenuBarController.swift # 状态栏图标 + 菜单
├── Monitor/ClipboardMonitor.swift # 多类型轮询解析
├── Panel/ # 浮窗
│ ├── PanelController.swift # NSPanel + 键盘事件
│ ├── PanelModel.swift # 筛选 + 搜索
│ ├── HistoryPanel.swift # SwiftUI 列表
│ ├── AppIconCache.swift # 来源 App 图标
│ └── ThumbnailCache.swift # 图片缩略图 LRU
├── Paste/Paster.swift # 多类型写回 + 模拟 ⌘V
├── Permission/PermissionHelper.swift # 辅助功能引导
└── Settings/
├── UserPreferences.swift # @Published UserDefaults
├── ShortcutRecorder.swift # SwiftUI 快捷键录制
└── SettingsView.swift # 通用 / 快捷键 / 隐私 / 关于
```
**总计**:27 个 Swift 文件,~2500 行代码,**零第三方依赖**。
---
## 💡 关键设计
- **轮询为何不慢**:每 tick 只读一个 `Int changeCount`,99% 的 tick 都是几纳秒整数比较。`DispatchSourceTimer + leeway 100ms + qos .utility` 让系统合并定时器唤醒,能耗几乎不可测。
- **浮窗不抢焦点**:`NSPanel` 用 `.nonactivatingPanel` 风格,关闭后焦点回到目标 App,模拟 ⌘V 才能粘贴到正确位置。
- **快捷键无需特殊权限**:Carbon `RegisterEventHotKey` 是系统级 API,不依赖辅助功能。只有模拟 ⌘V 才需要 Accessibility 权限。
- **缩略图缓存**:SwiftUI List 每次重渲染都会触发 row body,对 1MB+ PNG 直接 `NSImage(data:)` 会很卡。用 `NSCache` 按 hash 缓存解码后的小图,列表滚动丝滑。
- **向后兼容的 Codable**:ClipItem 手写 `init(from:)`,新增字段用 `decodeIfPresent + 默认值`,老 JSON 也能解码。
- **持久化策略**:防抖 1 秒合并多次写入;退出前主动 flush;文件损坏自动备份,不阻塞启动。
- **开机自启识别 .app 形式**:`LaunchAtLogin.isBundled` 检测 Bundle,未打包成 .app 时 UI 自动禁用并提示。
- **设置窗口激活技巧**:临时切到 `.regular` 让窗口能聚焦,关闭后 `windowWillClose` 恢复 `.accessory`。
---
## 🛠️ 开发
### 编译
```bash
swift build # Debug
swift build -c release # Release
```
### 直接运行
```bash
swift run -c release ClipBoard
```
### 设置 App 图标
放置位置:
```
Resources/AppIcon.icns
```
[`scripts/build-app.sh`](./scripts/build-app.sh) 会在打包 .app 时自动拷贝它,
[`Resources/Info.plist`](./Resources/Info.plist) 的 `CFBundleIconFile = AppIcon` 已经指好了。
根据你手上的素材格式,三种生成方式:
| 你手上有的 | 命令 |
|----------|------|
| `AppIcon.icns`(已经是终态) | `cp YourIcon.icns Resources/AppIcon.icns` |
| 单张 1024×1024 PNG | `./scripts/make-icon.sh path/to/icon-1024.png` |
| `AppIcon.appiconset` 目录(Xcode asset catalog) | `./scripts/icon-from-appiconset.sh path/to/AppIcon.appiconset` |
| 没有圆角 / 没透明的源图(含被错认成 PNG 的 JPEG) | `swift scripts/icon-roundify.swift path/to/icon.png Resources/AppIcon.icns` |
| 散的多尺寸 PNG | 改名成 [Apple 规范](https://developer.apple.com/library/archive/documentation/GraphicsAnimation/Conceptual/HighResolutionOSX/Optimizing/Optimizing.html#//apple_ref/doc/uid/TP40012302-CH7-SW2) 后用 `iconutil --convert icns YourIcon.iconset --output Resources/AppIcon.icns` |
> **💡 macOS 不会自动给图标加圆角**,必须自己把圆角和透明背景画进 PNG。如果源图是方块(或扩展名是 .png 实际是 JPEG),用 `icon-roundify.swift` 套一个标准 macOS Big Sur+ 蒙版(内容占 80.5%、圆角半径 18%、外侧透明):
> ```bash
> swift scripts/icon-roundify.swift ~/Downloads/your-icon.png Resources/AppIcon.icns
> ```
> 体检源图是否合规:`sips -g hasAlpha -g format ~/Downloads/your-icon.png`
> 必须 `hasAlpha: yes` + `format: png`,否则就要走 roundify。
生成后直接打包:
```bash
./scripts/build-app.sh
# Finder 查看 build/ClipBoard.app 即可看到新图标
```
### 图标不刷新 / Launchpad 看不到
macOS 有多层图标缓存(Dock / Launchpad / IconServices),换图标后经常需要手动清。
**Launchpad 不显示图标的两个根本原因:**
1. **App 不在 Launchpad 扫描目录** — Launchpad 只扫描 `/Applications`、`/Applications/Utilities`、`~/Applications`。从 `build/` 下 open 的不会出现。
2. **图标缓存没刷新** — 即便 .app 已经更新,旧图标仍然贴在缓存里。
**完整重置流程:**
```bash
# 1. 先在菜单栏退出 ClipBoard
# 2. 安装到 /Applications(必须!)
cp -R build/ClipBoard.app /Applications/
# 3. 清 Launchpad 布局 + Dock 缓存
defaults write com.apple.dock ResetLaunchPad -bool true
killall Dock Finder
# 4. 再次启动
open /Applications/ClipBoard.app
```
**顽固缓存(极少数情况):**
```bash
sudo rm -rf /Library/Caches/com.apple.iconservices.store 2>/dev/null
sudo find /private/var/folders/ -name com.apple.iconservices -exec rm -rf {} \; 2>/dev/null
touch /Applications/ClipBoard.app
killall Dock Finder
```
---
## 📦 分发打包
按使用场景分为三档:**本机自用** / **发给朋友** / **正式分发**。
### 一、本机自用(10 秒)
直接打包成 ad-hoc 签名的 `.app`,本机能跑,**别的电脑不行**。
```bash
./scripts/build-app.sh
open build/ClipBoard.app
```
如果想常驻使用:
```bash
cp -R build/ClipBoard.app /Applications/
open /Applications/ClipBoard.app
# 然后在设置里勾选"开机自启"
```
> `build-app.sh` 在没有 `CODESIGN_ID` 环境变量时会用 `-`(ad-hoc)签名。Apple Silicon 必须签名才能运行,所以 ad-hoc 是最低要求。
---
### 二、发给朋友(1 分钟,但有黄色提示)
打成 DMG,朋友拖拽即装。**未公证**的 DMG 在 Gatekeeper 下会弹窗警告,但可以让对方手动放行。
```bash
./scripts/build-app.sh # 先生成 .app
./scripts/make-dmg.sh # → build/ClipBoard-0.4.0.dmg
```
把 DMG 发给朋友,对方双击挂载,把 ClipBoard.app 拖进 Applications。
#### 对方首次打开时的处理
macOS Sequoia (15) 起,Gatekeeper 更严格,未公证 App 会出现 **"无法打开,因为 Apple 无法检查它是否包含恶意软件"**。两种解决方法(任选一):
**方法 A — 系统设置放行**
1. 双击 App → 出现警告 → 点击「完成」
2. 系统设置 → 隐私与安全 → 滚动到底部 → **「仍要打开」**
3. 再双击 App → 现在多了「打开」按钮
**方法 B — 命令行解除隔离属性**(无需系统设置)
```bash
xattr -d com.apple.quarantine /Applications/ClipBoard.app
```
> 原理:DMG/网络下载会被打上 `com.apple.quarantine` xattr,触发 Gatekeeper。手动移除即可。
> 想完全没警告?走第三档。
---
### 三、正式分发(需 Apple Developer 账号,$99/年)
走完整流程:**Developer ID 签名 → 公证(Notarization)→ 装订(Staple)**。这样下载就没任何警告。
#### 准备工作(一次性)
1. 加入 [Apple Developer Program](https://developer.apple.com/programs/)
2. 在 [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/certificates/list) 申请一张 **Developer ID Application** 证书并导入钥匙串
3. 在 [App-Specific Passwords](https://appleid.apple.com/account/manage) 生成一个 app-specific password(**不是 Apple ID 密码**),用于公证 API
#### 完整发布命令
```bash
# 1) 用 Developer ID 签名打包
export CODESIGN_ID="Developer ID Application: Your Name (TEAMID)"
./scripts/build-app.sh
./scripts/make-dmg.sh
# → build/ClipBoard-0.4.0.dmg
# 2) 提交公证(首次会上传到苹果,约 1-5 分钟)
xcrun notarytool submit build/ClipBoard-0.4.0.dmg \
--apple-id "you@example.com" \
--team-id "TEAMID" \
--password "abcd-efgh-ijkl-mnop" \
--wait
# 3) 把公证票据装订到 DMG(让 DMG 离线也能验证)
xcrun stapler staple build/ClipBoard-0.4.0.dmg
# 4) 验证
spctl --assess --type open --context context:primary-signature -v build/ClipBoard-0.4.0.dmg
# 应输出:accepted, source=Notarized Developer ID
```
#### 把凭证存到钥匙串(避免命令行明文密码)
```bash
xcrun notarytool store-credentials "ClipBoardNotary" \
--apple-id "you@example.com" \
--team-id "TEAMID" \
--password "abcd-efgh-ijkl-mnop"
# 之后只需:
xcrun notarytool submit build/ClipBoard-0.4.0.dmg \
--keychain-profile "ClipBoardNotary" --wait
```
#### 常见公证失败原因
| 报错 | 原因 | 解决 |
|------|------|------|
| `The signature does not include a secure timestamp` | 签名时没带 `--timestamp` | `build-app.sh` 已默认加上,确认没改 |
| `The binary is not signed with a valid Developer ID certificate` | 用了 ad-hoc 或 Apple Development 证书 | 用 **Developer ID Application** 证书 |
| `The executable does not have the hardened runtime enabled` | 没启用 hardened runtime | 脚本已加 `--options runtime` |
| `Invalid Team ID` | TEAMID 错 | 在 [Membership](https://developer.apple.com/account/#!/membership) 页面查 |
---
### 版本号升级
每次发版前,更新 [`Resources/Info.plist`](./Resources/Info.plist) 的两个键:
```xml
CFBundleShortVersionString
0.5.0
CFBundleVersion
5
```
DMG 文件名会自动取用 `CFBundleShortVersionString`。
---
### 自动更新(可选,未实装)
如果想让用户自动收到更新,推荐接入 [Sparkle](https://sparkle-project.org/):
1. 加入 SPM 依赖:`https://github.com/sparkle-project/Sparkle`
2. 自建一个 `appcast.xml`,托管到 GitHub Pages / 自己服务器
3. 用 [generate_appcast](https://sparkle-project.org/documentation/publishing/) 工具生成 EdDSA 签名的发布信息
4. 把生成的 `appcast.xml` URL 写进 Info.plist 的 `SUFeedURL`
未来 M5 会考虑接入。
---
### 上架 Mac App Store(可选)
需要的额外工作:
- 启用 **App Sandbox**(剪贴板监听 API 仍可用,但模拟 ⌘V 等动作受限,要走 entitlement)
- 替换签名证书为 **Apple Distribution**
- 在 App Store Connect 创建 App 记录并上传 `.pkg`(用 `xcrun altool` 或 Xcode Organizer)
- 通过 App Review(剪贴板类工具通常审核较严,需要清晰隐私说明)
> 我们目前不上架,走 Developer ID 直接分发即可。
---
## 📅 进度
| 里程碑 | 状态 | 内容 |
|--------|------|------|
| M1 MVP | ✅ | 状态栏 + 文本历史 + 点击回写 |
| M2 可用版 | ✅ | 持久化 + 全局快捷键 + 浮窗 + 自动粘贴 |
| M3 完整版 | ✅ | 图片/文件/RTF + 收藏/删除 + 类型筛选 + 偏好设置 |
| M4 可分发版 | ✅ | .app 包 + 开机自启 + 自定义快捷键 + 缩略图缓存 + DMG |
| M5 增强 | ⏳ | iCloud 同步 / Snippets / OCR / AI 摘要 |
详细设计请见 [PLAN.md](./PLAN.md)。
---
## 📜 License
MIT