news 2026/10/20 8:22:30

发布仅 2 天狂揽 900+ 星,冲上 GitHub 趋势日榜:tendedero 把截图“晾”到了屏幕顶上

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
发布仅 2 天狂揽 900+ 星,冲上 GitHub 趋势日榜:tendedero 把截图“晾”到了屏幕顶上

发布仅 2 天狂揽 900+ 星,冲上 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

2026 年 10 月 8 日的 GitHub 趋势日榜上,头部被数学语料、Rust 重写的 Adobe 级软件这类大项目占据,而榜单第 18 位塞进了一只“小不点”:一个个人开发者前一天刚发布的纯 Swift 原生 macOS 应用。榜单快照时它已累计 497 星,两天后总数停在约 913 星。这就是 tendedero——总共不到 2000 行 App 代码的菜单栏工具,功能只有一件:把你截的每一张图,像衣服一样“晾”在屏幕顶部的隐形衣绳上。

这篇文章从源码层面拆三件事:它如何在不截一帧屏的情况下“拦截”系统截图、它如何改掉 macOS 的截图设置又保证退出时归还、以及一个无网络、无账号、1.7 MB 的小工具凭什么两天冲上趋势榜。

一、先说清楚概念:它是截图的“收件箱”,不是贴图工具

中文社区最熟悉的同类工具是“截图+贴图”路线:Snipaste 是典型代表,后来的 PixPin 又叠了 OCR 与长截图。这类工具的交互是“主动”的——按键把当前画面“钉”在桌面上,再随意拖动。

tendedero 走了相反的路。它从不截图:你照常按 ⌃⇧5(或任何第三方工具),区别在于文件一落盘,就变成一张用木夹子挂在绳上的“卡片”,带着随机的小倾角:

/// Every photo hangs a little crooked, like on a real line. let tilt = Double.random(in: -2.5...2.5)

绳子本身是一根抛物线,LineView.swift 里的下垂量与屏幕宽度成正比、封顶 30pt:

static func sag(width: CGFloat) -> CGFloat { min(30, width * 0.018) }

未展示时,整个面板整体上移自身高度加 12pt,藏在屏幕顶缘之上;鼠标靠近菜单栏它才滑下来,像自动隐藏的 Dock。绳上容量按屏幕宽度动态计算(AppDelegate.swift 中为max(3, min(12, (panelWidth-200)/174))),挂满后最旧的卡片会悄悄掉下去,不会无限堆积。首次启动时还会把空绳展示 5 秒,提示“Take a screenshot and it will hang here”。

二、两天 913 星:一条可以核对的增长曲线

把已知事实排一下时间轴:

  • 仓库唯一初始提交时间为 2026-10-07 10:59(UTC-7),一次性提交 30 个文件、2,976 行(含 README 与构建脚本),作者为 Alejandro Buján,应用默认提供英/西双语;
  • 2026-10-08 的 GitHub 日榜快照(距发布约 30 小时)显示:累计 497 星,位列按 24 小时增速筛选的前 20 名第 18 位;
  • 发布两天后累计约 913 星。

分段计算:前 30 小时约 497 星,均值约 16 星/小时;第二天约 416 星 / 18 小时,均值接近 23 星/小时——增速在第二天明显抬升。这个量级对一个个人小工具而言已经很猛,而它的“含金量”在于背景:同一份榜单前列是 8,000+ 星单日增量的 AI 数学语料和重做 Photoshop 工作流的 Rust 项目,一只 1.7 MB 的纯本地小应用能挤进 Top 20,本身就是一次罕见的注意力事件。

另外两个容易被忽略的细节:代码 MIT 许可,但 README.md 明确声明“名称和图标不在许可范围内,fork 需要自己改名换标”;应用经 Developer ID 签名并通过 Apple 公证,还提供 Homebrew cask 安装:

brew install --cask alejandrobujan/tap/tendedero

这种“产品化”的收尾方式,在个人开源项目里不多见。

三、核心设计一:不截屏,所以不需要截屏权限

macOS 的“屏幕录制”权限是一道敏感门槛:任何调用屏幕捕获接口的应用都会触发系统授权弹窗,用户的第一反应往往是警惕。tendedero 用一个架构决策绕开了它,ScreenshotWatcher.swift 头部的注释写得很直白:

/// Tendedero never takes screenshots itself: you keep your usual shortcut /// (or CleanShot, or anything else) and the line just picks them up.

做法是“监听 macOS 保存截图的文件夹”:

  1. 确定目录:从com.apple.screencapture偏好域读保存位置。值得注意的是,面向 macOS 27 的正式版把这个 key 改名为location-screenshot且忽略旧 key,代码因此新旧两个 key 都读、都写——这是全项目最“贴身跟踪系统”的部分;
  2. 监听文件事件:对目录挂DispatchSourceFileSystemObject,监听 write/rename/delete;
  3. 0.2 秒防抖:因为 macOS 实际是“先写隐藏临时文件再改名”,文件出现瞬间内容可能未写完:
// macOS writes a hidden temp file and renames it; give it a moment. DispatchQueue.main.asyncAfter(deadline: .now() + 0.2, execute: work)
  1. 甄别真截图:监听桌面时只认带系统扩展属性com.apple.metadata:kMDItemIsScreenCapture的图片,防止把随手存的图挂上绳。

收益直接:⌃⇧4、⌃⇧5、任意第三方工具的产物都能被接住,且全程不申请屏幕录制权限。

四、核心设计二:Inbox 模式,借走系统设置、退出时归还

macOS 截图的默认路径是:存桌面 + 右下角浮动缩略图停留 5 秒。缩略图是仪式感,也是等待点——文件要 5 秒后才落定,桌面则越攒越乱。

Inbox.swift 的“Inbox 模式”干两件事:关掉浮动缩略图(文件即刻写盘)、把保存位置改到~/Library/Application Support/Tendedero/Screenshots,即直接写系统偏好域:

set(locationKey, folder.path) set(screenshotLocationKey, folder.path) set(thumbnailKey, false)

而“改系统偏好”这件事的工程边界感,体现在周围一圈处理上:

  • 只问一次:首次启动用 NSAlert 征求“Let Tendedero handle your screenshots?”,wasOffered标志防止反复打扰;
  • 先备份再改:三个 key 的原值存入自己的 UserDefaults;
  • 保证归还:退出时恢复;由于kill式终止不会触发applicationWillTerminate,AppDelegate.swift 额外为SIGTERM/SIGINT/SIGHUP注册了信号处理器,兜底执行恢复;
  • 安全网:Inbox 模式下另开一个桌面安全 watcher——注释写道,如果某个 macOS 版本无视了新设置(macOS 27 恰好改了 key 名),截图落在桌面上也照样会被挂上绳;
  • 可一键撤销:菜单栏可随时关闭该模式,关掉即还原。

对应的注释值得抄进团队规范:“Changing system settings is the user's call, never ours.”(改系统设置是用户的决定,永远不是我们的。)这是“高效”与“越界”之间的那条线。

五、核心设计三:像 Dock 一样隐藏,全屏时自我克制

面板是一个borderless + nonactivatingPanel的 NSPanel:floating 层级、canJoinAllSpaces + stationary + ignoresCycle、永不抢焦点。

真正的难点是鼠标事件:面板横跨整屏宽度,若吞掉所有点击会挡住下方一切。解法是 LineView.swift 用 SwiftUIPreferenceKey把每张卡片的真实 frame 回传给Line.hitRects,主循环(30fps 定时器)据此动态切换ignoresMouseEvents——只有指针在照片上时面板才接收点击,其余区域一律穿透。

显隐时序则是一组精心调过的阈值:

条件行为
鼠标在菜单栏“停留” 0.25s该屏幕的绳滑下(“短到感觉即时,长到路过菜单栏不会触发”)
鼠标离开绳区域 0.5s收回
点击菜单栏任意位置立即收起,防止“你刚开的菜单上又冒出一条绳”
⌃⌥T手动显隐

快捷键走 Carbon 的RegisterEventHotKey,HotKey.swift 的注释特意强调:这条路径与全局键监听不同,不需要辅助功能权限。

全屏是边界情况。FullScreen.swift 通过两个私有 CGS 接口查询当前 Space 是否为全屏 Space(type 4):

// These calls are private but stable for a decade, need no permission, // and are what window managers like yabai rely on.

注释很诚实:这两个调用私有但十年稳定、无需权限,正是 yabai 这类窗口管理器所依赖的手段。一旦有全屏应用(视频、演示),绳永远隐藏。全屏时不刷存在感,是这个小工具“高级感”的来源之一。

六、动效是人格:飞行、落绳、随风

让人愿意为演示 GIF 点赞的,是 CaptureFlight.swift 里的“飞行”动画:截图文件自带系统扩展属性kMDItemScreenCaptureGlobalRect(主显示器坐标系下的捕获区域),代码读出这个 rect 后,开一个无边框、不激活的整屏窗口,以 120fps 跑 0.65 秒的动画——

static let duration: CFTimeInterval = 0.65

轨迹是拱高 30pt 的 ease-in-out cubic;截图在飞行中缩小、按卡片的固定倾角扭转、玻璃相框与木夹的透明度从行程 35% 开始渐显——落地时“飞行的副本”与绳上真卡以 0.16 秒交叉淡入,肉眼看不出接缝。

被丢弃的卡片则从绳上“坠落”:下坠 520pt、倾角持续加大、0.55 秒淡出;回收站丢弃还会播放系统自带的“drag to trash”音效(代码直接引用 CoreAudio 组件里的 aif)。环境细节同样到位:每 7~16 秒来一阵“风”,各卡片随机错开、摆动 1.6~3.4°;挂绳时播放 “Tink”、掉落时 “Pop”。Line.swift 的注释把意图说破了:

/// Every so often a little wind moves the line. It is the detail that /// makes it feel like an object and not a widget.

(隔一阵小风吹动绳子,这个细节让它像个物件,而不是个控件。)

七、一套手势契约:点、按、拖,三个动词各管一件事

手势集极其克制,且每个拖拽落点语义唯一(GrabArea.swift):

手势效果
单击PNG 与文件 URL 写入系统剪贴板,随处可粘贴
长按 0.45s打开系统 Markup 标注
双击用预览(Preview)打开
拖到应用应用得到副本,卡片留在绳上
拖到文件夹文件移入该处,卡片离开绳
拖到废纸篓 / 点角落的叉丢弃(Inbox 截图进回收站,桌面上的只摘绳不动文件)
鼠标停在菜单栏绳滑下
⌃⌥T显隐

两个实现细节值得品:

  1. 拖拽的 operation mask 是[.copy, .move, .delete]——由接收方决定语义:应用取副本、Finder 取原件、废纸篓执行删除;松手在无接收方处时,animatesToStartingPositionsOnCancelOrFail = true让卡片“飞回”绳上,而不是生硬回弹;
  2. 标注不内置编辑器,而是调用系统com.apple.MarkupUI.Markup共享扩展(Markup.swift),用户点“完成”后由宿主进程把编辑结果原子写回原文件并刷新绳上缩略图。整个工具把标注、预览、Finder 全部“借”自系统,自己只保留“挂”这一个动作。

八、为什么偏偏是它:隐私、极简与“纯原生”的三重叙事

回到“为什么上榜”,答案大多写在 README.md 的 Tech Specs 表里:无账号、无网络、无分析、1.7 MB、免费、Developer ID 签名加公证。在“本地优先”这两年成为主基调的语境下,这些字段逐项得分。

但真正的差异化是零权限焦虑与零学习成本:

  • 零权限焦虑:不截屏(无需屏幕录制)、全局快捷键走 Carbon(无需辅助功能)、读 Space 状态用私有 CGS 接口(无需权限);首次运行唯一会触发的授权,是桌面文件夹访问(给桌面兜底 watcher 用),且可整体关闭;
  • 零学习成本:没有设置页,菜单栏菜单只有显隐、全部摘下、Inbox 开关、打开截图文件夹、声音、开机启动几项。

工程侧它同样是一份少见的“可读源码”样本:Sources/Tendedero下 14 个文件 1,953 行,加上 scripts/ 里用代码画图标与宣传图的make-icon.swift、make-readme-art.swift,总量约 2,600 行——仓库里甚至没有设计文件,README 还提供了一张“Inside the app”表,逐文件说明职责。本地化是 Localization.swift 里 8 行的L()函数,按首选语言判断英/西。

放到日榜语境里看逻辑就顺了:榜单前列用“Rust 重写 Adobe”“722 篇数学论文”拼存在感,这只 1.7 MB 的小工具拼的是一句“Your Desktop. Finally clear.”——痛点不大但人人可共情:每次截图后等待 5 秒的浮动缩略图、越来越乱的桌面。tendedero 没有创造新工作流,只是把截图的“位置”从堆在桌面移到悬在屏幕上方,再用系统 API 让这段动效像个真实物件。

两天 913 星的完整公式因此可以写出来:每个 macOS 用户都有的小痛点 + 无需解释的“本地、无权限”卖点 + 10 秒就能看懂的演示,外加一份愿意读源码的人可以逐行读完的实现。工具会迭代,但“把一件小事做到边界感清晰”这件事,它已经示范完了。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 9:59:13

一专多长:普通人如何科学规划年度学习方向与技能成长

1. 为什么说“一专多长”是普通人最值得下注的成长策略每年年初,朋友圈都会被各种学习Flag刷屏。有人一口气报了五个课程,有人列了长达三页的书单,但到了三月份再看,大部分都已经静悄悄。我见过太多这样的案例,也踩过同…

作者头像 李华
网站建设 2026/10/19 23:38:18

PHQ9抑郁自评去哪做

PHQ-9 抑郁症状自评去哪做 想在网上做 PHQ-9 抑郁自评,要的是量表本身正规、答案不被收集、结果边界写明。明白健康(https://healviews.com/)的测评中心里有 PHQ-9 抑郁症状自评 https://healviews.com/assessments/phq9 :9 题、约…

作者头像 李华
网站建设 2026/10/17 13:40:02

基于PLC的污水处理组态王动画仿真系统设计

1. 这套系统到底在做一件什么事接触过工控项目的人应该都有感受:PLC程序好写,画面也能画,但真正把“设备控制逻辑”和“上位机监控画面”打通,让泵该起的时候起、该停的时候停,画面上所有状态和趋势跟现场一走——这个…

作者头像 李华
网站建设 2026/10/14 23:21:53

基于PCA9422与PIC18F66K40的嵌入式电源管理系统设计实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 13:34:11

PCA9422与PIC18F67K40协同实现可编程电源管理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/16 8:05:53

sqlite-netFx40-2010.rar 实战:.NET 连 SQLite 的 32/64 位兼容与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华