☰
两天 913★ 的“晾衣绳”是怎么想出来的:tendedero 交互点子全拆解
2026/10/9 22:29:52 网站建设 项目流程

两天 913★ 的“晾衣绳”是怎么想出来的: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

2026 年 10 月上旬,一个叫 tendedero 的 macOS 小工具在 GitHub 上两天拿下 913 颗星,挤进日榜头部。它做的事听起来小到离谱:把截图挂到屏幕顶部的一条绳子上。但正是这个"小",让它在一众截图工具里显得格外扎眼——Snipaste 拼标注、PixPin 拼长截图、Screenshot-to-Code 拼 AI 转代码,而 tendedero 只回答一个问题:截图拍完之后,去哪了?

这篇文章会从它的名字与隐喻说起,拆开顶栏那条"悬停即滑落"的绳子的完整交互闭环,再回到源码层,看它如何用不到 20 个源文件、1.7 MB 的体积,把"不做管理、不做标注、不做历史"的极简决策,做成了让用户愿意打星的理由。

隐喻即文档:把理解成本降到零

tendedero 在西语里就是"晾衣绳"。项目的自我描述只有一句话:Screenshots, hung out to dry(把截图晾起来)。这不是一句文案,而是整套交互的元模型。

截图工具的既有范式是"贴纸":截图之后,图片以浮动缩略图、贴图、剪贴板内容的形式存在。贴纸模型需要用户理解"临时"与"持久"的边界——这张图是粘在屏幕上,还是进了剪贴板?贴完会不会丢?而晾衣绳模型是生活经验自带的:绳子上的东西看得见、挂得住、随取随用,收下来才属于自己,丢进垃圾桶才算不要了。README 首页的题眼Out of sight. Within reach.(看不见,但够得着)把这套语义翻译成了产品的价值主张:截图不消失,也不打扰你,它就挂在屏幕边缘待命。

这个隐喻不是口号,而是被写进了代码的数据模型。在 Line.swift 里,每一张截图被建模为一个Pegged结构体——字面意思就是"被夹住的东西":

struct Pegged: Identifiable, Equatable { let id = UUID() let url: URL var thumb: NSImage /// Every photo hangs a little crooked, like on a real line. let tilt = Double.random(in: -2.5...2.5) var falling = false var flying = false }

注意那个tilt:每张图挂上去都会带一个 -2.5° 到 2.5° 的随机倾角。真实晾衣绳上的照片不会整整齐齐,这个随机数就是隐喻在数据层的最小体现。文件本身的注释也毫不掩饰设计意图:The files never move. The line is only a view onto them.(文件从不移动,绳子只是它们的一层视图)。绳子是"看待截图的视角",不是"存储截图的容器"——这个抽象直接决定了后面所有极简决策。

一条绳子的完整交互闭环

晾衣绳隐喻解决的是"认知"问题,但真正让产品成立的是那条绳子的"物理"行为。整套交互围绕屏幕顶部一条 210 pt 高的透明面板展开(LinePanel.swift),而核心交互循环只有三个动作:悬停滑落、停留取用、离开收起。

悬停滑落。默认状态下绳子藏在菜单栏上沿之外(panelHeight + 12的负偏移),像是 Dock 自动隐藏的效果。鼠标顶到屏幕顶部边缘时,AppDelegate 开始计时:AppDelegate.swift 里revealDelay = 0.25秒,注释写得很坦白——"短到感觉是即时的,又长到让一次快速路过菜单栏不会误触"。计时通过一个 30Hz 的Timer轮询光标位置(startMouseTracking),同时还会先判断目标屏幕是否被全屏应用占用。关键代码在tick():

// Resting in the menu bar brings the line down on that screen. // Pushing against the top edge is part of it, and it also works // when another display sits above and the pointer never stops. if let screen = screenUnderPointer, inMenuBar, !menuBarSuppressed, !FullScreen.isActive(on: screen) { let since = hotZoneSince ?? now hotZoneSince = since if now.timeIntervalSince(since) >= Self.revealDelay { ... reveal() } }

细节藏在注释里:把"顶部边缘"定义成菜单栏区域(menuBarBand(of:)处理了隐藏菜单栏的边界情况),意味着在多显示器纵向堆叠时,光标"顶着上沿"也能触发。绳子从哪块屏滑出,取决于光标在哪块屏——用NSScreen.screens.first { NSMouseInRect(mouse, $0.frame, false) }找到"指针所在屏",因为截图刚拍在哪儿,人就在哪儿。

停留取用。绳子滑下后,点击即复制,双击用 Preview 打开,长按 0.45 秒唤起系统 Markup 编辑器,右击弹上下文菜单,拖拽则交给系统级拖放——GrabArea.swift 里的GrabView把每张照片桥接成真正的文件拖拽源。最有心思的是拖放语义的分流:拖进 App 是"复制一份,原图留在绳上";拖进文件夹是"文件归你,照片下绳";拖到废纸篓则整张消失。同一个物理动作,因为落点不同而语义不同,NSDraggingSession的sourceOperationMaskFor用.copy/.move/.delete三个操作掩码实现了这套分流。复制成功时,卡片下方还会浮起一个 "Copied" 玻璃胶囊提示(PeggedView.swift),1.2 秒后自动消退。

离开收起。光标离开绳子区域 0.5 秒(retractDelay),绳子缩回屏幕顶部之外,整个屏幕恢复原状。收起时用的是easeIn(duration: 0.22),滑出时是spring(response: 0.42, dampingFraction: 0.82)——收起更快更"果断",滑出更"活",两套曲线在 LineView.swift 的offset动画里分工明确。

围绕这套主循环,还有三个收尾细节值得注意。一是命中测试的透传:面板本身ignoresMouseEvents = true,只有光标落在照片卡片上(hitRects记录的偏好值)时才把鼠标事件还给面板(updateMousePassThrough),保证绳子覆盖的整条屏幕区域对下层应用"隐形"。二是全屏应用的规避:FullScreen.swift 通过调用窗口服务器的私有 CGS API 判断当前 Space 类型,全屏视频或演示时绳子绝不出现——注释里写着"视频或演示不该有晾衣绳横在顶上"。三是菜单栏点击压制:任何菜单栏区域的点击都会让绳子立刻收起(menuBarSuppressed),避免它盖住正在弹出的菜单。

"飞"进绳子的截图,和它背后的一整套接管逻辑

如果仅仅是把截图文件夹里的图展示出来,tendedero 不过是又一个看板。它真正的工程野心体现在两个动作里:截图"飞"上绳子,以及接管 macOS 的截图设置。

截图飞行。macOS 在截图文件的扩展属性里记录了截图时的屏幕区域(com.apple.metadata:kMDItemScreenCaptureGlobalRect)。CaptureFlight.swift 用getxattr把这个矩形读出来,然后新建一个覆盖全屏的无边框透明面板,用 Core Animation 图层让截图从拍摄位置起飞:0.65 秒内沿一条最高点 30 pt 的抛物线(sin(.pi * k) * arc)飞到绳上的落点,途中同步完成"缩小、倾斜、长出玻璃相框和夹子"的形变——update()里对宽度、高度、圆角、inset 逐帧插值,夹子clip.frame在落点时刻恰好咬住卡片顶部。降落后浮层淡出,真实的 SwiftUI 卡片在其下方接手。也就是说,用户看到的是"截图从屏幕中央飞起来挂到绳上",而不是"一张图突然出现在绳子上"。丢弃时也有对称的动画:卡片从绳上坠落 520 pt、旋转、淡出,同样由CaptureFlight.fall在全屏浮层上绘制,避免被绳子的窄条裁切。

这个飞行还有一个隐性的信息增益:它顺带告诉用户"截图拍完了、系统处理好了、图片在这儿",把 macOS 自带浮动缩略图的那 5 秒等待,压缩成了 0.65 秒的视觉确认。

接管截图设置。首次启动时应用会弹出一个确认框(AppDelegate.swift 的offerInbox),问用户是否让 tendedero"接管"截图。一旦同意,Inbox.swift 会做两件事:把com.apple.screencapture域的show-thumbnail设为 false(关掉浮动缩略图,文件立即落盘而不是 5 秒后),并把保存位置改成自己的 Application Support 子目录。用户的原始设置会被备份,退出或关闭时原样还原。源码里甚至处理了两个极端情况:macOS 27 把配置键从location改名成location-screenshot,所以两个键都会写;万一系统某次忽略了设置、截图还是落到桌面,AppDelegate.swift 里的safetyWatcher会同时盯着桌面目录兜底,把漏网之鱼也挂上绳子。

再配合 ScreenshotWatcher.swift 的目录监听(DispatchSource.makeFileSystemObjectSource监听.write/.rename/.delete,延后 0.2 秒等 macOS 完成"写临时文件再改名"),整条链路是:截图落盘 → 目录事件 → 扫描识别 → 读取扩展属性定位拍摄区域 → 起飞动画 → 挂绳。注意 watcher 里onlyTaggedScreenshots的设计:监听桌面时只认带kMDItemIsScreenCapture扩展属性标签的真截图,避免把用户桌面上的普通图片误挂上绳;而 tendedero 自己的目录里任何图片都算数——毕竟在它自己的地盘上,图片都是它自己接管的截图。

极简取舍:不做标注、不做管理,为何反而赢了

tendedero 的能力清单短得反常:点击复制、长按标注、双击打开、拖拽分发、右上角叉号丢弃,外加一个⌃⌥T显示/隐藏的快捷键。没有图库、没有历史列表、没有标注工具栏常驻、没有账号同步。整个交互表只有七行(README 的 "A gesture for everything." 一节),每张卡片的交互密度却极高——一个手势对应一个系统能力,绝不重叠。

这种克制在代码里体现为三层:

系统能力复用。长按标注没有自研编辑器,而是通过NSSharingService调用系统的 Markup 扩展(Markup.swift),拿不到时优雅降级到 Preview。双击打开交给NSWorkspace,拖放交给 AppKit 原生拖拽会话。tendedero 自己只做"调度"和"视觉",不做任何图片编辑功能。

零网络零账号。README 的技术规格表里,"Network access" 一栏写的是 None。整个代码库搜索不到任何网络请求,数据持久化只有一行UserDefaults(Line.swift 里storeKey = "pegged"存的是文件路径数组)。截图文件从未被移动或复制进应用内部——save()存的是url.path,重启后按路径重建绳子,文件一直在用户的文件夹里。对截图这类敏感资产而言,"你的截图从不离开你的 Mac"(README:Private by design.)不是营销话术,而是架构事实。

从渲染层开始的"真实性"。极简不等于简陋,绳子的"物感"来自一层层刻意叠加的细节:绳子本身用三层描边画出"中灰核心 + 高光 + 阴影",两端用透明度渐隐制造"从屏幕外延伸进来"的错觉(LineView.swift 的Rope);每张照片随机倾斜;每 7~16 秒会有一阵"风"让卡片轻轻晃动(Line.swift 的scheduleGust,注释直言"这个细节让它像个物件而不是控件");按住卡片会缓慢压扁到 0.95 倍,营造"长按在蓄力"的感觉;卡片玻璃相框用系统ultraThinMaterial加顶部高光描边,注释还特意说明"不做折射,让背景保持锐利"。

值得注意的是,README 里的所有视觉素材——hero 图、demo 动图、bento 交互图——都是scripts/make-readme-art.swift用代码渲染出来的,连应用图标都是scripts/make-icon.swift画的。一个截图工具,它的宣传素材全是程序生成的,这本身就是"用代码解决问题"气质的延伸。

回到那个问题:913 颗星买的是什么

把时间轴拉回 2026 年 10 月 7 日(仓库最近一次提交)到 10 月 8 日登上日榜,913 颗星的增长窗口很短,短到不可能是功能积累的产物,只能是"第一次见面就被记住"的结果。人们记住 tendedero,不是因为某个杀手功能,而是因为一个反直觉的产品决策:在截图工具都在做加法的时代,它选择了一个隐喻、一条绳子和一套手势,然后停下。

晾衣绳隐喻把理解成本降到了零——用户不需要读教程、不需要学图库概念,晾衣绳上挂着照片这件事任何人都秒懂;顶栏一条线 + 悬停滑落 + 离开收起,构成了一个不需要状态栏图标点击、不需要快捷键记忆的完整闭环;而不做标注、不做管理、不做同步的极简取舍,反而让每个保留下的交互都足够深(拖到不同目的地有不同的语义)。README 里那句Same shortcuts. Same muscle memory. Just less mess.(快捷键照旧、肌肉记忆照旧,只是更干净)点出了它的位置:它不是替代 Snipaste 的编辑器,而是替 macOS 截图流程收拾残局的"最后一步"。

源码目录里 14 个 Swift 文件、1.7 MB 的成品、MIT 许可——所有这些都指向同一件事:好的交互点子不一定复杂。一个能自我解释的名字,加上一套自洽的物理隐喻,有时候就足够让上千人愿意在自己的屏幕上,给这条晾衣绳留一条缝。

【免费下载链接】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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询