两天冲上 GitHub 日榜头部:tendedero 凭什么让程序员集体把桌面“清空”?
【免费下载链接】tendederoScreenshots, hung out to dry. A tiny native macOS app that hangs every screenshot on a line at the top of your screen.项目地址: https://gitcode.com/gh_mirrors/te/tendedero
10 月 7 日上午,一个只有单个 commit 的 Swift 仓库悄悄上线;48 小时后,它出现在 GitHub 日榜速报第 4 位,累计 Star 逼近 1000。这个叫 tendedero(西班牙语“晾衣绳”)的小应用只做一件事:把每张截图像衣服一样挂在屏幕顶部的晾衣绳上,让截图永远不必再落在桌面。没有账号、没有网络、1.7 MB、单人开发、约 2000 行代码。本文基于公开的日榜数据快照与仓库源码,还原这次起量的时间线,拆解它“寄生”在 macOS 系统缝隙里的三处工程巧思,并对剩余的热度窗口做一个不吹不黑判断。
一、48 小时时间线:从首个 commit 到日榜头部
先把能证实的事实摆出来(仓库 git 历史 + GitHub 日榜速报系列的两期数据):
| 时间 | 事件 |
|---|---|
| 10-07 10:59(UTC-7) | 仓库唯一一次提交d991454:30 个文件、2976 行新增,17 个 Swift 源文件 + 构建脚本 + 宣传图一次性齐备;README 同时写明 Developer ID 签名、Apple 公证与 Homebrew cask 安装方式 |
| 10-08 早间快照 | 首次进入日榜速报,第 18 位,约 497 Star |
| 10-09 早间快照 | 日榜升至第 4 位,991 Star,24 小时净增 494 |
三个细节值得注意:
- 仓库只有一次提交。没有“先放出来再迭代”的痕迹,签名、公证、Homebrew、多语言(英/西)全部是首版就有的“信任设施”。热度的起爆点是作品本身加上传播渠道,而不是运营动作。
- 次日增量(+494)几乎等于首日总量(497)。曲线在第二个 24 小时还没有走平,这是判断热度窗口是否关闭的核心依据(后面再展开)。
- 作者 Alejandro Buján 用极低的代码密度换极高的完成度。全部源码不到 2000 行,但文件职责切得极细:
LinePanel管透明面板、ScreenshotWatcher管文件监听、Inbox管系统设置接管、CaptureFlight管飞行动画——每个文件 40~120 行,几乎没有一处样板代码。
二、痛点普遍性:截图堆桌面是跨平台顽疾
“截图文件堆在桌面”是个老痛点,且在社区里有长长的内容生态做背书:掘金系平台上,Snipaste 的贴图功能、PixPin“平替 Snipaste”(长截图、OCR)、FastStone Capture 的对比评测,从 2019 年到 2024 年持续产出,共性叙事都是“截图→标注→粘贴/分享”这条链路,以及“文件别到处乱飞”的焦虑。
macOS 用户对这个痛点尤其具体,tendedero 的源码注释把它说得很精确(见 Inbox.swift):
- 截图默认落在桌面,日积月累就是一堆
Screenshot 2026-10-07 at 10.59.55.png; - 截完图右下角会浮出一张缩略图,等满 5 秒才真正写入文件。
tendedero 的 README 用两行话概括对策:“No floating thumbnail. No five-second wait.”(没有浮动缩略图,没有五秒等待)。
但真正聪明的是它的竞争位。它不碰“截图”这个动作本身——ScreenshotWatcher.swift 的注释写得明白:
Tendedero never takes screenshots itself: you keep your usual shortcut (or CleanShot, or anything else) and the line just picks them up. (tendedero 从不自己截图:你继续用惯用的快捷键,晾衣绳只负责“接住”它们。)
它只接管“落盘”之后这一步。这意味着它不与 Snipaste、PixPin、CleanShot 抢快捷键和肌肉记忆,而是吃掉所有截图工具共同的下游产物——文件。定位避开了红海,这也是它能快速被“截图工具疲劳人群”接受的原因。
三、工程拆解:约 2000 行代码如何与 macOS “共存”
这一节是全文的重点。tendedero 没有任何自研截图能力,它的价值全部体现在与系统 API 的协作方式上。
3.1 用 kqueue 监听截图落盘,而不是自己截屏
ScreenshotWatcher.swift 对 macOS 保存截图的目录建立文件描述符监听:
let src = DispatchSource.makeFileSystemObjectSource( fileDescriptor: fd, eventMask: [.write, .rename, .delete], queue: .main) src.setEventHandler { [weak self] in self?.scheduleScan() }两个容易被忽略的工程细节:
- 0.2 秒去抖。macOS 写截图是“先写隐藏临时文件再 rename”,注释原文:“macOS writes a hidden temp file and renames it; give it a moment.” 事件触发后延迟 200ms 再全量扫描目录,避免读到半截文件。
- 扩展属性白名单。如果监听的是桌面,只接受带
com.apple.metadata:kMDItemIsScreenCapture这个 xattr 标记的文件——那是 macOS 给真实截图打的内标,防止桌面里随便存的图片被误挂上绳。
更体现作者功力的是版本兼容:macOS 27 把截图保存位置的首选项键从location改名成了location-screenshot,而旧版本只读location。所以Inbox写入时两个键都写,读取时先查新键再查旧键,并在读取前显式CFPreferencesAppSynchronize强制从 cfprefsd 取最新值——因为设置可能在运行时被它自己改掉。
3.2 Inbox 模式:借走系统设置,退出必还
接受首次启动的提示后,tendedero 会接管截图的两个系统设置:关闭浮动缩略图(show-thumbnail = false,文件立刻写入)、把保存位置改到~/Library/Application Support/Tendedero/Screenshots。桌面从此只会出现你主动拖出去的那部分截图。
但“改系统设置”是个敏感动作,它的防御性设计值得抄作业(Inbox.swift + AppDelegate.swift):
- 改之前先把旧值(location / location-screenshot / show-thumbnail)存进 UserDefaults,
restore()时原样放回; - 恢复不只挂在
applicationWillTerminate——普通用户kill进程或注销并不会触发它,所以又注册了 SIGTERM / SIGINT / SIGHUP 的信号源,信号到来时先Inbox.restore()再退出。注释解释得很直白:“so macOS is never left pointing at a folder nobody is watching.”(绝不让 macOS 把截图存进一个没人看的文件夹); - 如果某个 macOS 版本无视了设置改名(27 的键名问题),还会在桌面上开一个安全哨兵 watcher,漏到桌面的截图照样挂上绳。
“接管必须可逆、崩溃也要可逆”——这是小工具建立长期信任的关键,也是它敢在 README 里写 “Private by design. No account. No network. No analytics.” 的底气。
3.3 晾衣绳面板:一个从不抢焦点的悬浮窗口
承载晾衣绳的 LinePanel.swift 只有 41 行,但每一行都是为“不打扰”服务的 AppKit 技巧:
super.init(contentRect: .zero, styleMask: [.borderless, .nonactivatingPanel], ...) level = .floating collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle] ignoresMouseEvents = truenonactivatingPanel+canBecomeKey == false:面板永不需要焦点,点了它也不会抢走当前应用的键盘上下文;ignoresMouseEvents = true是默认值——面板横贯整块屏幕,但只有照片区域能接住鼠标。AppDelegate.swift 里一个 30fps 的定时器(tick())每帧检查光标位置,仅当落在某张卡片hitRects上(外扩 4pt 容差)时才把ignoresMouseEvents翻成 false;- 收放逻辑模仿自动隐藏 Dock:光标在菜单栏停留 0.25 秒(
revealDelay)绳滑下来,移开 0.5 秒(retractDelay)收回。新截一张图还会自动“探头”2.5 秒让你看到,然后缩回。
另一个硬骨头是全屏应用。FullScreen.swift 调用了CGSMainConnectionID/CGSCopyManagedDisplaySpaces这对私有 CoreGraphics 服务 API——注释承认它们是私有的,但“稳定了十年、无需权限,yabai 这类窗口管理器也靠它”——通过判断当前 Space 的 type 是否为 4(全屏)来决定“视频和演示进行时,屏幕顶部绝不出现晾衣绳”。这是整份源码里最“灰色”的一段,作者选择了用注释把风险边界写明。
3.4 “飞上绳”动画:从文件元数据反推截图位置
最出圈的视觉——截图从原位置飞起、缩小、长出玻璃相框和木夹、落上绳——实现在 CaptureFlight.swift,它靠的是读取文件上的第二个扩展属性:
// com.apple.metadata:kMDItemScreenCaptureGlobalRect // macOS 把截图区域的 [x, y, w, h] 以 plist 形式存在文件上 let size = getxattr(path, name, nil, 0, 0, 0)拿到捕获区域后(注意坐标系转换:系统元数据以主屏左上角为原点,要翻成 AppKit 的左下角原点坐标),用一个 120fps 的 Timer 驱动 CALayer 补间:位置走 ease-in-out cubic,同时用 smoothstep 在进度 35%→100% 区间渐入“chrome”(玻璃边框、高光边、木夹)——飞行物在途中就变成最终形态,落地时无需任何切换,PeggedView的真实卡片在下面淡入、飞行窗口 0.16 秒淡出交接。
顺带一提,Line.swift 里每张卡片天生带 ±2.5° 随机倾斜,每 7~16 秒随机来一阵“风”让所有卡片轻晃——注释说这是“让它像个物件而不是控件”的细节。这类纯氛围代码大约占源码 10%,却是传播的主要燃料。
3.5 无按钮手势语法:一次拖拽,三种归宿
所有交互集中在 GrabArea.swift 的GrabView: NSView, NSDraggingSource上:
| 手势 | 行为 | 实现要点 |
|---|---|---|
| 单击 | 复制图片 | 往NSPasteboard同时写 PNG 数据 + fileURL,粘贴到任意 App 都成立 |
| 长按 0.45s | 系统 Markup 标注 | 通过NSSharingService调起com.apple.MarkupUI.Markup扩展(Markup.swift),编辑结果原子写回原文件,绳上缩略图即时刷新 |
| 拖进应用 | 复制一份,留在绳上 | 拖拽会话对外声明[.copy, .move, .delete],App 取走的是 copy |
| 拖进文件夹/桌面 | 文件留下,卡片自动离绳 | Finder 取走 move;结束后延迟 0.6s 复查(“Finder finishes a move a moment later”),文件不在原位就prune() |
| 拖进废纸篓 | 删除 | Dock 的 Trash 接受 delete 操作后由 App 自己调FileManager.trashItem,并播放系统自带的 “drag to trash.aif” 音效 |
| ⌃⌥T | 显式收放 | 走 CarbonRegisterEventHotKey(HotKey.swift),注释强调:与全局键监控不同,它不需要辅助功能权限 |
容量与持久化也都有讲究:绳上最多挂几张照片不是写死的,而是屏幕宽度 / 174pt夹在 3~12 之间(updateCapacity());挂绳状态用 UserDefaults 存路径数组,重启后恢复;文件被手动删掉的卡片会通过prune()自动“掉落”,连掉落音效Pop和挂绳音效Tink都做了可开关。
3.6 分发与许可:把“可信”写进首版
- 应用 1.7 MB,macOS 14+(Apple silicon 与 Intel 通吃),Developer ID 签名 + 公证,
brew install --cask一行安装; - 从源码构建只需 Swift 工具链,Xcode 可选(Package.swift 就是一个 10 行的 executable target);
- README 里所有宣传图——包括 icon——全部由代码绘制:
scripts/make-readme-art.swift(411 行)、make-icon.swift、make-dmg.sh,连 DMG 背景都不用人美工件; - LICENSE 是 MIT,但明确排除名称与图标:“代码是 MIT,名字和图标不是,所以 fork 得有自己的。” 这种把品牌边界写进许可证条款的做法,在个人项目里少见。
四、热度能撑多久:起量窗口的判断
基于两期日榜快照(10-08 与 10-09 各 24 小时窗口)和仓库状态,可以给出一个尽量冷静的判断:
窗口还没关,但只剩 1~2 天。依据:
- 首日 497 Star → 次日 +494/24h,增量曲线仍在加速而非衰减;10-09 快照时它已冲到日榜第 4 位。日榜按 24h 增量排序,只要增量维持在这个量级,它大概率还会在榜上停留 1~2 天,累计 Star 有希望摸到 1500~2000 区间;
- 但日榜对“次日增量”极其敏感:一旦 10-10 窗口的增量掉到 200 以下(这类单功能小工具在爆发 48~72 小时后的典型水平),就会迅速滑出榜单,进入长尾。
哪些因素会拉长它的长尾:痛点刚性强(几乎每个 Mac 用户都中招)、安装零门槛(本地优先、无账号、无网络,试错成本约等于零)、信任设施齐全(签名/公证/Homebrew/设置可逆)。这些决定了它不会像很多榜上一日游的仓库那样“掉榜即死”,大概率沉淀出几百个真实用户。
哪些因素会压住它的天花板:
- 单人维护 + 单 commit 历史,快照窗口内未见 CI、测试或 issue 讨论的公开活动。Star 冲过 2000 之后必然出现第一批 issue(多显示器旋转、外接屏排序、与 CleanShot 的边界情况),响应速度会直接决定口碑曲线;
- macOS 14+ 单平台决定了它对日榜上非 Mac 用户只是“看个动画就走”,这部分流量转化率天然偏低;
- 私有 CGS API 依赖虽“稳定十年”,但任何系统大版本都可能带来适配成本,这是一个悬在维护者头上的长期变量。
综合来看,tendedero 这 48 小时更像一次产品力验证而非运营事故:它在截图工具这个被 Snipaste、PixPin 盘踞多年的红海里,用“不抢截图、只接落盘”的错位定位和一组干净的系统 API 工程,完成了首次曝光到破千 Star 的闭环。接下来 72 小时的关键变量只有一个——作者能否把 991 个 Star 转化成 issue 区里的第一批真实用户反馈。对读者而言,如果它最终退榜,这套“监听 + 接管 + 可逆”的工程模板,仍然是一份值得留在书签里的 macOS 系统协作范例。
【免费下载链接】tendederoScreenshots, hung out to dry. A tiny native macOS app that hangs every screenshot on a line at the top of your screen.项目地址: https://gitcode.com/gh_mirrors/te/tendedero
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考