跳到正文

上线时间 2026年9月

档案卡 · 第 147 号

Phantomat

一个 Hyprland 插件:把所有窗口放到一块无限画布上,打几个字母,机位就飞到那个窗口。

Screenshot of Phantomat
编辑截图, 1 Nov 2026Phantomat ↗

这是什么

平铺窗口管理器把桌面分成工作区,预览只会让你看一眼别的窗口,然后把你放回原处。开着一堆窗口时,一天里有一部分时间花在记「哪个窗口在哪个工作区」上,第二台显示器再添一套要记的位置。

Phantomat 让预览本身变成桌面。每个窗口在一块平面上有固定位置,显示器是看向这块平面的机位;缩小视图时,镜头从一片弯曲的透镜里退后,输入标题的一部分,机位就飞到对应的窗口。工作区变成画布上的「地点」,一个窗口可以横跨两台显示器之间的接缝。

它是插件,不是合成器。它加载进 Hyprland,用函数钩子接管工作区的渲染路径,窗口模型仍然用 Hyprland 自己的。作者 fork 了 hyprland-scroll-overview,保留上游历史,用 Claude Opus 5.5 在一天里加上画布这一层;提交尾注和发布推都写了这件事。

产出的系统代码是认真的,支持面却很窄:没有发布版,只能从源码安装,有一批开着的 issue,README 说它只在一种配置上日常使用。Hyprland 一更新,插件就可能要重新编译才能用。

谁做的

英国朴茨茅斯的设计工程师。X 简介写的是「Fractional design engineer for startups. Clients backed by YC, $20M+ raised. 2 slots for November.」;GitHub 账号 2014 年创建,24 个公开仓库,多数是视觉作品,包括 105 星的 Three.js 地形图 monolith-terrain、Three.js 感知镜头 machine-sight、GLSL 屏保 omarchy-cosmos-screensaver。fork 之后的 14 个提交里有 9 个是他做的,9 个都把 Claude Opus 5.5 记成共同作者。

它是怎么搭起来的

组成 · 8

Phantomat 把桌面当成一个有相机的场景,把 Hyprland 当成借来的渲染器。每个被管理的窗口在世界坐标里有一个盒子;每台显示器持有相机偏移和缩放;渲染路径画出每台相机看到的那一块。插件没有重写合成器。它钩住画工作区、排帧、路由 surface damage、摆 popup 的那几个函数,用画布场景换掉工作区场景。这个选择保住了 Hyprland 的窗口模型、输入路由和协议支持,也把维护负担压在它钩住的那些内部函数签名上。画布层被拆成职责很窄的模块:Navigator 管搜索和选择,Hud 管覆盖层,BarrelShader 管透镜,Memory 管持久化,Config 管 Lua 设置面,Window 管 X11 和弹窗的坐标翻译。最大的文件 scrollOverview.cpp 仍然是画布状态和渲染循环碰头的地方。

main.cpp
插件入口和生命周期。声明 14 个 CFunctionHook 指针,装上和卸下,暴露 PLUGIN_API_VERSION;已经有别的预览插件占着钩子时,弹一条通知然后失败。
scrollOverview.cpp / scrollOverview.hpp
画布本身:CScrollOverview,每台显示器一个,持有相机偏移、缩放、窗口盒子、联屏相机共享、全屏、填充和固定状态、渲染循环和输入转发。.cpp 有 9,305 行。
Navigator.cpp
打字查找:折叠文字,按标题、类名、分类给词打分,排序,画应用图标,实现方向键的几何规则和 Alt-Tab 切换。
Hud.cpp / Tuning.cpp
覆盖层和实时调参:面板、小地图、按键表、四角读数,以及把每次调整写进调参 Lua 文件的设置清单。
BarrelShader.cpp / shaders/barrel.frag
最终 pass 的透镜:桶形畸变、边缘模糊、暗角、红蓝边缘;HUD 和小地图在畸变之后合成。
Memory.cpp
布局持久化:XDG_STATE_HOME 下的 TSV 文件,最多 96 个窗口条目,每台显示器一个相机,读进来的每个值都做合理性检查。
Config.cpp / examples/spatialoverview.lua
Lua 设置面:plugin.spatialoverview 下的 96 个配置值和六个 Lua 函数,外加安装脚本复制的示例配置。
tests/ and scripts/
17 个嵌套 Python 测试和 C 辅助程序,加上安装、卸载和安全卸载脚本。

取舍,以及另一种做法

  • fork hyprland-scroll-overview,不自己写合成器 替代 新写一个合成器,或做一个独立的预览

    上游提供了 Hyprland 插件的管线、工作区渲染器和历史里已有的修复;fork 保住了这段历史和署名。代价是 9,305 行的 scrollOverview.cpp,以及要背着走的 386 个上游提交。

  • 钩住 14 个合成器函数 替代 只用公开插件 API

    自定义工作区渲染器和自定义输入路径不在公开 API 里;钩子是画布能换掉工作区场景的原因。代价是版本脆弱:#20 说在 Hyprland 0.57 上构建失败。

  • 默认把几台显示器当成一张联起来的桌子 替代 每台显示器一块独立的画布

    联屏相机让窗口跨过接缝时位置连续,两块屏幕像一个空间;canvas.linked_screens = false 保留了独立相机模式。代价是更复杂的焦点和全屏模型。

  • 产品改名之后保留 spatialoverview 这套内部名字 替代 把所有名字都换掉

    插件 id、设置文件、dispatcher 和 hyprctl 命令继续对旧配置有效;改的只是人读到的地方。代价是产品里有两个名字,以及 README 里一段解释。

  • 布局存普通 TSV,设置存 Lua 替代 数据库或设置界面

    记忆文件能直接看,坏一行不会毁掉全部,也小到可以读完;Lua 文件和 Omarchy 的配置方式一致,实时调参器可以边改边写。代价是第二个调参文件,以及用户必须知道的加载顺序。

依据kaolti/phantomat 的 main 分支(2026-11-02 读取):README.md、docs/window-rules.md、main.cpp、scrollOverview.cpp、scrollOverview.hpp、Navigator.cpp、Memory.cpp、Config.cpp、BarrelShader.cpp、shaders/barrel.frag、Hud.cpp、Tuning.cpp、Window.cpp、tests/、scripts/install.sh、hyprpm.toml、CMakeLists.txt、examples/spatialoverview.lua;另读 GitHub API 里的九个提交信息和文件清单。

制作过程

7 个阶段
  1. 01

    平铺窗口管理器藏起来的那个桌面

    Hyprland 把窗口平铺进工作区。一个工作区就是一屏;按一下键切到另一个,原来那屏的窗口就看不见了。预览是各家合成器都有的东西:按住一个键,看见缩小的窗口,挑一个,预览关掉。桌面仍然是一叠屏幕,不是一个空间。 Phantomat 冲的就是这个缺口。它的 README 把自己叫作可缩放的无限画布窗口管理器:所有窗口住在同一块没有边的平面上,而不是住在工作区里。显示器是看向这块平面的相机,不是平面本身。缩小不是挑完窗口就结束的模式;画布一直在,100% 的视图只是同一块画布拉近之后的样子。 作者把动机写进了发布推:I wanted infinite canvas for my desktop. So I built it with Omarchy and Opus. 目标用户是开着很多窗口、不止一台显示器的人,那种已经把预览用得很熟、希望它干脆别关的人。 Phantomat 不是合成器。它是一个 Hyprland 插件,要求 Hyprland 足够新、能读它用的 Lua 配置。这把受众收窄到 Hyprland 上的 Linux 用户,多数是作者自己用的 Omarchy;也是 README 的安装一节从「Hyprland 0.56 或更新」开始的原因。

  2. 02

    一个设计工程师、一次 fork,和一天里的 Opus

    作者是 Zsolt Kacso,GitHub 和 X 上的 kaolti。他的公开仓库大多是视觉作品:monolith-terrain,一个用 Three.js 做的地形图,105 星;machine-sight,一个 Three.js 的感知镜头;omarchy-cosmos-screensaver,一个 GLSL 屏保。Phantomat 是其中的异类:一个 C++ 系统插件。 它是 fork。yayuuu/hyprland-scroll-overview 是一个像 Niri 那样滚动工作区的 Hyprland 插件,创建于 2026-04-18,515 星、47 个 fork,到 2026-11-01 还在推送。Phantomat 的 README 把功劳记给 yayuuu、Daniel Skorupa、Vaxry 和 Hypr 开发团队,上游历史也留在仓库里。GitHub API 数出 main 上 400 个提交:386 个来自上游,14 个发生在 fork 之后。 画布这一层就在这 14 个里。九个出自 Kacso,日期都是 2026-09-24,从 08:05:18 到 22:38:31 UTC,14 小时 33 分钟。九个提交全带 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>。它们合计加 18,209 行、删 3,195 行。第一个提交就动了 62 个文件,加 14,087 行、删 2,950 行,scrollOverview.cpp 一处 +6,538/−2,160,同时新增 Navigator.cpp、Hud.cpp、BarrelShader.cpp、Memory.cpp、Tuning.cpp、Icons.cpp、Popups.cpp、透镜着色器、安装脚本和一套嵌套测试。 最后一个提交之后十八分钟,22:56:09Z,他发了发布推:I wanted infinite canvas for my desktop. So I built it with Omarchy and Opus. I'm calling it Phantomat. 推里带一段 47 秒、3840×2160 的演示视频;截至 2026-11-02 的读数,1,181 个赞、75 次转发、69 条回复。 fork 之后的另外五个提交是别人的。Paul Harfas 三个,Pablo 一个,Eo 一个。Pablo 那个把 HUD 和小地图在旋转显示器上画正的提交,带的是 Cursor 的共同作者尾注。尾注写的是模型,不是宿主:仓库里没有任何地方说明用的是 Claude Code、别的 harness,还是混着用。仓库能证明的是,九个提交就是作者的全部贡献,而且每一个都记了 Claude Opus 5.5。

  3. 03

    画布既是渲染器,也是输入路由

    main.cpp 是入口。它声明 14 个 CFunctionHook 指针,钩住画布必须替换的合成器函数:renderWorkspace、两条 damage 路径、scheduleFrame、sendFrameEventsToWorkspace、surface frame、两条 drawTex 路径、popup 重定位、拖动目标开始,以及三条 X11 configure 和 message 路径。PLUGIN_API_VERSION 返回 HYPRLAND_API_VERSION,所以一次构建和它编译时用的头文件绑死。 CScrollOverview 是画布对象,每台显示器一个。它持有相机偏移和缩放,把窗口的世界坐标盒子映射到屏幕盒子(canvasWorldToScreen),也能反过来(canvasScreenToWorld),并决定哪块画布画哪些窗口。canvas.linked_screens 打开时,几台显示器显示同一块平面上相邻的部分,相机一起动;关掉后,每台显示器有自己的相机。头文件里是画布需要的操作:landOnWindow、summonWindow、fitAllWindows、panCamera、zoomCameraBy、nudgeWindow、canvasRedo、followCanvasWindow、checkpointCanvas、saveSharedCanvasLayout。 钩子就是设计本身。Hyprland 的公开插件 API 不提供自定义的工作区渲染器,也不提供自定义输入路径,所以插件替换掉画工作区、处理 surface 帧的那几个函数,然后画自己的场景。X11 窗口能在画布上用,也是同一套拦截:一个 X11 客户端只知道 X 服务器手里的位置,而在画布上,那个位置往往不是窗口被画出来的地方。Window.cpp 把画出来的位置重新报给客户端,再把客户端请求的位置翻译回去。弹出菜单固定在屏幕上画,不跟着画布走,因为一个在变换后的坐标上打开的菜单,鼠标点不到。 这是它同时很有力和很脆弱的地方。接管渲染路径的画布可以把所有窗口放到一个平面上;它也接过了和合成器内部之间的契约,而这个契约在 Hyprland 版本之间会变。

  4. 04

    导航器、透镜、HUD,以及记住位置的那几个文件

    看得见的那一层由四个程序组成。Navigator.cpp 689 行:把文字折叠成可比较的形式,给每个词在窗口标题、类名和分类上打分,排序,再把带应用图标的面板画出来。方向键用 Android 的规则:候选窗口必须在那个方向上两条边都伸得更远,并且落在这一侧的 45° 以内;和当前窗口同一行或同一列的优先。Alt+Tab 用同一套选择模型切换最近窗口。 BarrelShader.cpp 和 shaders/barrel.frag 是透镜。片段着色器 208 行,在一次最终 pass 里做完桶形畸变、边缘模糊、暗角、红蓝边缘。小地图和搜索面板在透镜之后合成,所以桌面弯了,字还是平的。Hud.cpp 1,070 行,画面板、小地图、按键表和四角读数;Tuning.cpp 440 行,是实时调参器,每改一项就写进 spatialoverview-tuning.lua。 Memory.cpp 最小,却让画布有「东西还在原处」的感觉。它写一个制表符分隔的文件,位置在 $XDG_STATE_HOME/spatial-overview/canvas-memory.tsv(没有就用 ~/.local/state/...),最多留 96 个窗口条目,每台显示器存一个相机,读回来的几何值只要不是有限数或大得离谱就丢掉。坏一行只让那个窗口丢记忆,不会毁掉整个文件。 Config.cpp 是设置面:96 个 addConfigValueV2 调用、六个 Lua 函数,全在 plugin.spatialoverview 下,从 canvas.linked_screens、canvas.persistent 到 navigator.accent、distortion.chromatic。hyprctl spatialoverview 把画布状态打成 JSON。实时调参器写第二个 Lua 文件,在主配置之后加载,所以调过的值会赢,直到你把那一行删掉。

  5. 05

    它对机器有什么要求

    构建是 C++23 加 CMake,旁边还有 Makefile、Meson 和 Nix flake 三条路。目标链接 hyprland、hyprgraphics、pangocairo、pixman-1、libdrm、libinput、libudev、wayland-server、xkbcommon 和 Lua 5.4。安装脚本检查 g++ 15 以上、make、pkg-config、python3 和这些开发包;Arch 或 Omarchy 上的包是 base-devel git hyprland hyprgraphics pango lua。 scripts/install.sh 114 行,干的比复制一个文件多。没有 ~/.config/hypr/hyprland.lua 就不跑;先比对已安装的 Hyprland 头文件和正在运行的合成器,再编译 spatialoverview.so,装到 ~/.local/share/spatial-overview/,用户没有配置就复制一份 examples/spatialoverview.lua,备份 hyprland.lua,追加一段带标记的加载代码,最后 hyprctl plugin load。scripts/uninstall.sh 把这段删掉,恢复原来的布局。 GitHub 上没有发布版,也没有 tag。安装从一份 checkout 开始,README 要求 Hyprland 更新之后重跑安装脚本,因为插件是对着头文件编译的。hyprpm.toml 也在,给 Hyprland 的插件管理器用,构建命令是 make all,但 README 支持的路径是 shell 安装脚本。 机器要求按模型的标准算低,按桌面的标准算高:除了测试用的 NVIDIA 配置,没有点名显卡型号;但需要 Hyprland 0.56 或更新、带 Lua 配置,需要新到 GCC 15 的 C++23 编译器,还需要和正在运行的合成器一致的头文件。代价是安装时间和从源码构建带来的维护。

  6. 06

    十七个测试文件、十五个开着的 issue,一种配置

    测试在窗口里跑一个嵌套的 Hyprland,用 Python 驱动。tests/ 下有 17 个文件,其中 15 个是 *-nested.py,另有 tests/nested.lua、一个 X11 测试程序、一份虚拟指针协议描述,和一个测帧率的探针。名字对应规则:navigator、fullscreen、fullscreen-cases、keys、linked、fill、places、arrows、drag、popup、tuner、framerate、multimonitor、x11、install。窗口规则文档写明每个测试查的不变量:其它窗口不动,窗口保持浮动并且还被画着,两块屏幕停在同一视图,缩出去再缩回来什么都不变。 测试是仓库里最能说明「和功能同一天出生」的部分。帧率修复的提交信息说,一个窗口之前只有 15 fps,修完 60;tests/framerate-nested.py 是测量,tests/tools/fps-probe.py 是那个每一帧都动画的窗口。X11 那个提交加了 tests/x11-nested.py、tests/tools/x11-menu.c 和 tests/tools/wl-click.py。 支持面比测试清单窄。2026-11-02 时仓库有 15 个开着的 issue 和 pull request,没有发布版,也没有 tag。2026-10-30 开的 #20 说在 Hyprland 0.57.0 上构建失败,因为 desktop/Workspace.hpp 没有了。#21 说一个可见窗口在 240 Hz 下被限到 15–60 fps。#18 说画布模式下拖放坏了。README 把原因写得很直:This hooks deep into Hyprland internals, so a Hyprland update can break it until it is updated too. It has been used daily on one setup (Omarchy, Hyprland 0.56.2, NVIDIA, two monitors); other setups are less tested. 代码树里没有 CI 目录。嵌套测试是手动跑的,最后一次推送是 2026-10-08,比第二波关注早三周。

  7. 07

    246 颗星、一段发布视频、第二波关注

    2026-11-02 的 GitHub 读数是 246 星、20 个 fork、1 个 watcher、15 个开着的 issue 和 pull request、0 个发布版、0 个 tag,BSD-3-Clause。仓库创建于 2026-09-24T08:07:06Z,比第一个提交的时间晚两分钟;最后一次推送是 2026-10-08T09:45:55Z。它 fork 的上游 yayuuu/hyprland-scroll-overview 是 515 星、47 个 fork。 第一波是发布推。2026-09-24 的 22:56:09Z,Kacso 发了那段 47 秒的演示视频,配一句「想要无限画布,于是用 Omarchy 和 Opus 做了出来」。读数时是 1,181 个赞、75 次转发、69 条回复。回复都很短,也很技术:一个人说他几年前试过无限画布的概念,喜欢这里做的选择;一个人说这适合 Vision Pro;还有一个人只说好。 第二波是一次创作者推荐。2026-10-31 的 21:00:09Z,@OmarchyUpdates 发了一条短推荐。这是一个社区运营的账号,1,878 个关注者,简介写 Independent Omarchy updates: releases, PR watch and fixes you can copy。那条推有 121 个赞、11 次转发、6 条回复、67 次收藏;X 的 trending 页 Omarchy Linux Expands to Phones, TVs and Easy Installs 把它和 hop、OmarchyTV 放在一起。第二波来的时候,代码已经三周没动了;这是对想法的关注,不是对新版本的关注。 和上游比,这是一个 fork 的数字:246 星对 515 星,自己的 14 个提交对上游的 386 个。和发布推比,星数比赞数小。视频起了作用,而仓库自己的最后一次推送早于第二波。

他会告诉你什么

  • 九个提交全部带 Claude Opus 5.5 尾注,在 14 小时 33 分钟里加 18,209 行、删 3,195 行。这是 AI 写出来的增量。另外 386 个提交是上游插件,246 颗星压在两者之上。模型该拿的是画布这一层的功劳,不是底下 Hyprland 插件管线的功劳。
  • 发布推有 1,181 个赞和一段 47 秒的 4K 视频;仓库有 246 颗星。传播出去的是视频。发布五周后社区账号那条 121 个赞的第二波,来的时候最后一次推送已经过去三周。
  • 9,305 行的 scrollOverview.cpp 是 fork 的代价。第一个提交在里面改了 6,538 行,又在周围加了十个新模块;新代码是分层的,它插进去的那个文件不是。审画布的人得读一个 9,305 行的编译单元。
  • 14 个内部函数钩子既是机制,也是风险。#20 说 Hyprland 0.57 挪了一个头文件,构建就断了;README 说一次更新可能让插件失效,直到重新编译。接管渲染路径的插件,押的是会动的内部 API。
  • 支持面是一种配置。README 写的是 Omarchy、Hyprland 0.56.2、NVIDIA、两台显示器;#21 报 240 Hz 下只有 15–60 fps,#18 报拖放坏了,仓库里有 15 个开着的 issue,没有发布版。测试是真的,测试没有覆盖 issue 来自的那些配置。
  • 尾注是共同作者署名,不是逐行归属。它能证明模型在每一个提交的桌前;它说不出哪些行是模型写的。真正记录意图的是提交信息,那些文字读起来像行为规格。
  • 改名是一个兼容决定。产品叫 Phantomat;插件 id、设置文件和 dispatcher 仍然是 spatialoverview,README 解释旧配置为什么还能用。Lem 来的名字是给人看的那一半,旧名字是必须稳住的那一半。

相关档案

全部档案 →