Phantomat:把工作区取消掉,让所有窗口活在同一张无限画布上

  • 一个窗口可以横跨接缝,一半在左屏一半在右屏,而且不会被画两次。
  • 把它从一块屏拖到另一块屏,是一整段连续的位移,松手就停在那儿。
  • 平移、缩放、落到某个窗口上,都会同时带动所有屏幕 —— 因为那就是同一张画布。

和「每屏一个独立相机」的做法比,这个方案的代价是要处理一堆边界情况:窗口归属哪块屏(答案是「显示它最多的那块」)、每屏各自全屏、X11 应用以为自己在哪里……项目专门写了一份 docs/window-rules.md,把「每一条 Hyprland 按键在画布上意味着什么」逐条写死,而且每条规则后面都标注了由哪个测试覆盖。

真正的硬骨头:让「平铺的按键」在画布上说得通

这是整个项目最容易被低估的部分。Hyprland 有一大套围绕平铺和工作区的按键:方向键聚焦邻居、SUPER + 1..0 切工作区、SUPER + J/P/L 调平铺布局、SUPER + HOME 移动工作区……到了画布上,这些键要么得重新赋予含义,要么得明确地「什么都不做」,而且必须在每一种窗口状态下都成立:窗口在一块屏上、横跨接缝、在屏幕外、被 fill 过、已经退到缩略视图。

项目给每种情况定的判据很硬:「没有别的窗口移动 / 窗口保持浮动且被绘制 / 两块屏处在同一个视图层级 / 退开再回来什么都不变」。例如 SUPER + G 建组的键,在画布上被显式定义为「什么都不做」,理由写在文档里 —— 在画布上堆叠的组,会让 Hyprland 在退出时崩溃。这种「把不支持的东西写清楚并让它安全地无效」的做法,比悄悄给个半吊子行为要靠谱得多。

全屏、填充、钉住:三个各自独立的语义

  • 填充(SUPER + T)把窗口撑满它所在的那块屏,算上 gaps 和边框;再按一次回到原来的位置和大小。中途移动过就以新位置为准,中途缩放过就重新撑满。
  • 全屏(SUPER + F)只让那一块屏的画布让开,其他屏幕该什么样还什么样。每块屏可以各自有一个全屏窗口。Wine / Proton 游戏那种「无边框窗口 + 覆盖整屏」会被识别成全屏,退回窗口模式时恢复到游戏自己要的尺寸;游戏在「战斗闪一下」时不会退出全屏;游戏藏起光标做鼠标视角时指针老老实实待在那块屏上。
  • 钉住(SUPER + O)让窗口钉在屏幕上不动,画布平移时它悬在画布之上;再按一次放回画布当前位置。

另外还有一档实验性的「地点」(canvas.places = true):工作区键被映射成画布上的一个个位置,SUPER + 1..0 飞过去,Shift + SUPER + N 把当前窗口搬过去,SUPER + TAB 去隔壁那个(哪怕它是空的)。去的时候 minimap 上会显示你正飞向哪里。默认是关的,因为这套语义还在打磨。

图标解析:不打包任何图片,直接问你的桌面

画布上要给每个窗口画图标,项目没有自带任何图标资源。它做的是按启动器的规矩去解析:拿窗口的 app id 去匹配 .desktop 文件,依次尝试 StartupWMClass、文件名、Name;拿到 Icon= 之后,在「当前图标主题 → 它 Inherits 的主题(递归,深度上限 6)→ hicolor → /usr/share/pixmaps」这条链上按尺寸子目录顺序找 SVG 或 PNG。

里面有几个只有真实使用才会踩到的补丁:

  • 反向域名 id 的兜底:org.gnome.Nautilus 这种 app id,桌面文件常常只叫 nautilus,所以要拿最后一个点后面的部分再查一次。
  • Omarchy 的网页应用:它们是 chromium --app=URL 跑起来的,窗口类名长成 chrome-<host>__-Default。项目在扫 desktop 文件时顺手把 Exec 里的 URL host 记下来,再拿 host(必要时逐段砍掉后面的路径)去反查图标。
  • 缓存按 path@size 建键,超过 256 个就整体清空;更重要的是,它提供了一个 resolve() —— 在动画开始之前先把文件系统扫描做掉,而不是在动画中间去扫盘。

调参:所见即所得,而且不会覆盖你的配置

在退开的视图里按 CTRL + ,,搜索框会原地变成设置列表。打字筛(blur、grid dot、accent),上下选,左右调,改一下就能立刻看到;Shift 微调、CTRL 粗调、Delete 撤销这一项。

可调的项接近 80 个:镜片强度 / 边缘模糊 / 暗角 / 色散 / 过渡时机、网格的间距线宽点径样式、minimap 的位置透明度、HUD 的字体和字号和圆角、视差壁纸的速度和深度、智能整理时的尺寸相似度阈值、窗口自动放置的碰撞间隙……

这里有一处工程上很正确的处理:调参结果写到单独的文件 ~/.config/hypr/spatialoverview-tuning.lua,它在 spatialoverview.lua 之后加载。所以升级不会覆盖你的设置,而调过的值会「赢」到你手动删掉那一行为止。同理,scripts/install.sh 也只往 hyprland.lua 里加两行带标记的语句,加之前先备份。

关于「改了名字但内部没改」这件小事

项目原来叫 Spatial Overview,改名成 Phantomat 之后,设置文件名、hl.plugin.spatialoverview.* 动作、hyprctl spatialoverview 命令全部保留了旧名,就是为了让改名之前的配置继续能用。这种取舍看起来不优雅,但比强行统一再让所有人重写配置要友好。

安装

需要 Hyprland 0.56 或更新(并且是它带 Lua 配置的那套)、GCC 15 或更新,以及 Hyprland 的开发文件。Arch / Omarchy 上:

sudo pacman -S --needed base-devel git hyprland hyprgraphics pango lua
git clone https://github.com/kaolti/phantomat.git
cd phantomat
scripts/install.sh

安装脚本会:编译插件 → 放到 ~/.local/share/spatial-overview/ → 备份并往 hyprland.lua 插两行 → 立刻加载。然后按 SUPER + CTRL + G 就能看到效果。不想用了跑 scripts/uninstall.sh,桌面恢复原状、设置文件保留(要连设置一起删就加 --purge)。

必须说清楚的代价

这是一款深度 hook 进 Hyprland 内部的插件,README 自己写得很坦白:Hyprland 一升级,插件就需要重新编译,否则会直接提示「这个 build 是给另一个 Hyprland 版本用的」,跑一遍安装脚本即可。它目前只在一套配置上被长期日常使用(Omarchy + Hyprland 0.56.2 + NVIDIA + 双屏),其他组合的测试覆盖要弱得多。

另外它是在 yayuuu/hyprland-scroll-overview 的基础上长出来的 fork,上游代码和历史都保留在仓库里 —— 包括那个 480KB 的 scrollOverview.cpp。两个项目之间没有隶属或背书关系。

为什么值得一看

它最值得看的地方不是「无限画布」这个点子 —— 这个点子本身不新 —— 而是把点子落地成了什么:用自己写的 GLSL 兑现镜片效果、把多屏的边界情况一条条写成规则再一条条挂上测试、给系统主题做 icon 回退、把调参结果隔离到单独文件、在不支持的行为上选择「安全地什么都不做」并说明理由。这些东西都不会出现在截图里,但决定了它能不能真的每天用。

顺带一提,9 月 26 日刚发布的 phantomat 同期还有一位邻居:同一个 Omarchy 生态里的 omarchy-meeting-recorder,把麦克风和系统声音录成两条独立的轨再转写。看得出来这个围绕 Omarchy 的小工具生态正在快速长出来。

封面图按项目自己的视觉语言生成:桶形镜片变形与暗角直接复刻自 shaders/barrel.frag 的公式。项目本身不打包任何图标资源,所以没有可用的官方 logo。

先把「无限画布」这件事说清楚

Hyprland 上管窗口,传统思路是「工作区 + 平铺」:窗口被切成互不重叠的格子,工作区像抽屉一样一个叠一个。Phantomat 走的是完全相反的一条路 —— 它把工作区取消了,让所有窗口都活在同一张无限的二维平面上。

这个平面的名字有点中二:出自斯坦尼斯瓦夫·莱姆《技术大全》(Summa Technologiae)里的 “phantomat” —— 一台能在人周围造出整个世界的机器。项目用这个名字,大概是想说「整张画布就是你看到的世界」。

项目地址:github.com/kaolti/phantomat,BSD-3-Clause 协议,2026 年 9 月 24 日建仓,三天冲到一千多星。作者是在 Omarchy(DHH 那个「有主见的 Arch 发行版」)上日常用出来的。

它到底做了什么不同的事

平铺窗口管理器把屏幕当成一张纸,窗口是纸上的方块,位置没有意义。Phantomat 反过来:窗口的位置本身是有意义的。你把一个窗口放在另一个窗口右边,它就一直待在右边;你把它们拉开一屏远,它们就真的差一屏。

要看到全局,按 SUPER + CTRL + G。这一步不是普通的「概览」:镜头会穿过一圈弯曲的镜片退开,整个桌面像被吸进一个桶形镜头里,带着边缘发散、暗角和色散。再按一次回到 100%。

这个镜头不是贴图,是项目自己写的 GLSL。关键的一段在 shaders/barrel.frag 里:

// Radial lens mapping. Signed strength lets the user move continuously
// between barrel and pincushion distortion.
vec2 warped = centered * (1.0 + distort * radius2) / max(contrast, 0.001);
vec2 uv = warped * 0.5 + 0.5;

// 0 at the center of the screen, 1 in its corners.
float radius = sqrt(radius2) * 0.70710678;
float ramp = smoothstep(min(edgeBlurStart, 0.99), 1.0, radius);
vec2 outward = radius > 0.0001 ? centered / sqrt(radius2) : vec2(0.0);
// The fringe has its own, wider falloff: it grows from near the
// center, quadratically, as in a real lens.
float fringeRamp = smoothstep(0.1, 1.0, radius);
vec2 fringe = outward * chromatic * fringeRamp * fringeRamp * 0.014;

distort 是有符号的,所以同一个参数能从桶形畸变连续滑到枕形畸变。edgeBlur 那段用的是「黄金角圆盘采样」,越靠圆心越密,模仿真实镜头的散景。而且 HUD 和 minimap 是在这个 shader 的最后一步合成的,意味着搜索框的文字永远不会被镜片拧弯 —— 这个细节在很多「花哨效果」的实现里都会翻车。

键鼠操作:键盘优先,但鼠标也没被落下

退开之后可以直接打字搜索。这不是简单的过滤:

  • 每个词都必须出现,且字母必须连着。打 fas 能找到 fastfetch;因为每一个字母都必须按顺序相邻,搜索几乎不会给出含糊的结果。
  • 大小写和重音符号全部忽略。
  • 镜头和焦点会跟着「最匹配的那个窗口」走 —— 你一边打字,画面一边自己往目标滑。
  • 命中后用 Enter 直接在 100% 落上去,Shift + Enter 则是把它搬到你现在的位置再落上去。

搜索框旁边的操作是全套的:方向键选最近的窗口、CTRL + 数字 直达第 N 个结果、CTRL + 0 把所有匹配的窗口框进视野、CTRL + A 按应用把窗口整理成阵、CTRL + Z / CTRL + Shift + Z 撤销重做移动和整理。鼠标这边:点窗口飞过去、拖窗口就是移动、拖空白就是平移画布、滚轮缩放、点 minimap 跳转。

值得一提的是 ALT + TAB:在画布上它被重写成了「最近使用顺序」的切换器,轻点一下换到上一个窗口,按住 ALT 会直接把列表摊在屏幕上,松开就跳过去。

两块屏幕是同一张桌子

这是我觉得设计得最讲究的一部分。在 canvas.linked_screens(默认开)下,多个显示器展示的是同一张画布上相邻的部分,按显示器的实际摆放位置排列。于是:

  • 一个窗口可以横跨接缝,一半在左屏一半在右屏,而且不会被画两次。
  • 把它从一块屏拖到另一块屏,是一整段连续的位移,松手就停在那儿。
  • 平移、缩放、落到某个窗口上,都会同时带动所有屏幕 —— 因为那就是同一张画布。

和「每屏一个独立相机」的做法比,这个方案的代价是要处理一堆边界情况:窗口归属哪块屏(答案是「显示它最多的那块」)、每屏各自全屏、X11 应用以为自己在哪里……项目专门写了一份 docs/window-rules.md,把「每一条 Hyprland 按键在画布上意味着什么」逐条写死,而且每条规则后面都标注了由哪个测试覆盖。

真正的硬骨头:让「平铺的按键」在画布上说得通

这是整个项目最容易被低估的部分。Hyprland 有一大套围绕平铺和工作区的按键:方向键聚焦邻居、SUPER + 1..0 切工作区、SUPER + J/P/L 调平铺布局、SUPER + HOME 移动工作区……到了画布上,这些键要么得重新赋予含义,要么得明确地「什么都不做」,而且必须在每一种窗口状态下都成立:窗口在一块屏上、横跨接缝、在屏幕外、被 fill 过、已经退到缩略视图。

项目给每种情况定的判据很硬:「没有别的窗口移动 / 窗口保持浮动且被绘制 / 两块屏处在同一个视图层级 / 退开再回来什么都不变」。例如 SUPER + G 建组的键,在画布上被显式定义为「什么都不做」,理由写在文档里 —— 在画布上堆叠的组,会让 Hyprland 在退出时崩溃。这种「把不支持的东西写清楚并让它安全地无效」的做法,比悄悄给个半吊子行为要靠谱得多。

全屏、填充、钉住:三个各自独立的语义

  • 填充(SUPER + T)把窗口撑满它所在的那块屏,算上 gaps 和边框;再按一次回到原来的位置和大小。中途移动过就以新位置为准,中途缩放过就重新撑满。
  • 全屏(SUPER + F)只让那一块屏的画布让开,其他屏幕该什么样还什么样。每块屏可以各自有一个全屏窗口。Wine / Proton 游戏那种「无边框窗口 + 覆盖整屏」会被识别成全屏,退回窗口模式时恢复到游戏自己要的尺寸;游戏在「战斗闪一下」时不会退出全屏;游戏藏起光标做鼠标视角时指针老老实实待在那块屏上。
  • 钉住(SUPER + O)让窗口钉在屏幕上不动,画布平移时它悬在画布之上;再按一次放回画布当前位置。

另外还有一档实验性的「地点」(canvas.places = true):工作区键被映射成画布上的一个个位置,SUPER + 1..0 飞过去,Shift + SUPER + N 把当前窗口搬过去,SUPER + TAB 去隔壁那个(哪怕它是空的)。去的时候 minimap 上会显示你正飞向哪里。默认是关的,因为这套语义还在打磨。

图标解析:不打包任何图片,直接问你的桌面

画布上要给每个窗口画图标,项目没有自带任何图标资源。它做的是按启动器的规矩去解析:拿窗口的 app id 去匹配 .desktop 文件,依次尝试 StartupWMClass、文件名、Name;拿到 Icon= 之后,在「当前图标主题 → 它 Inherits 的主题(递归,深度上限 6)→ hicolor → /usr/share/pixmaps」这条链上按尺寸子目录顺序找 SVG 或 PNG。

里面有几个只有真实使用才会踩到的补丁:

  • 反向域名 id 的兜底:org.gnome.Nautilus 这种 app id,桌面文件常常只叫 nautilus,所以要拿最后一个点后面的部分再查一次。
  • Omarchy 的网页应用:它们是 chromium --app=URL 跑起来的,窗口类名长成 chrome-<host>__-Default。项目在扫 desktop 文件时顺手把 Exec 里的 URL host 记下来,再拿 host(必要时逐段砍掉后面的路径)去反查图标。
  • 缓存按 path@size 建键,超过 256 个就整体清空;更重要的是,它提供了一个 resolve() —— 在动画开始之前先把文件系统扫描做掉,而不是在动画中间去扫盘。

调参:所见即所得,而且不会覆盖你的配置

在退开的视图里按 CTRL + ,,搜索框会原地变成设置列表。打字筛(blur、grid dot、accent),上下选,左右调,改一下就能立刻看到;Shift 微调、CTRL 粗调、Delete 撤销这一项。

可调的项接近 80 个:镜片强度 / 边缘模糊 / 暗角 / 色散 / 过渡时机、网格的间距线宽点径样式、minimap 的位置透明度、HUD 的字体和字号和圆角、视差壁纸的速度和深度、智能整理时的尺寸相似度阈值、窗口自动放置的碰撞间隙……

这里有一处工程上很正确的处理:调参结果写到单独的文件 ~/.config/hypr/spatialoverview-tuning.lua,它在 spatialoverview.lua 之后加载。所以升级不会覆盖你的设置,而调过的值会「赢」到你手动删掉那一行为止。同理,scripts/install.sh 也只往 hyprland.lua 里加两行带标记的语句,加之前先备份。

关于「改了名字但内部没改」这件小事

项目原来叫 Spatial Overview,改名成 Phantomat 之后,设置文件名、hl.plugin.spatialoverview.* 动作、hyprctl spatialoverview 命令全部保留了旧名,就是为了让改名之前的配置继续能用。这种取舍看起来不优雅,但比强行统一再让所有人重写配置要友好。

安装

需要 Hyprland 0.56 或更新(并且是它带 Lua 配置的那套)、GCC 15 或更新,以及 Hyprland 的开发文件。Arch / Omarchy 上:

sudo pacman -S --needed base-devel git hyprland hyprgraphics pango lua
git clone https://github.com/kaolti/phantomat.git
cd phantomat
scripts/install.sh

安装脚本会:编译插件 → 放到 ~/.local/share/spatial-overview/ → 备份并往 hyprland.lua 插两行 → 立刻加载。然后按 SUPER + CTRL + G 就能看到效果。不想用了跑 scripts/uninstall.sh,桌面恢复原状、设置文件保留(要连设置一起删就加 --purge)。

必须说清楚的代价

这是一款深度 hook 进 Hyprland 内部的插件,README 自己写得很坦白:Hyprland 一升级,插件就需要重新编译,否则会直接提示「这个 build 是给另一个 Hyprland 版本用的」,跑一遍安装脚本即可。它目前只在一套配置上被长期日常使用(Omarchy + Hyprland 0.56.2 + NVIDIA + 双屏),其他组合的测试覆盖要弱得多。

另外它是在 yayuuu/hyprland-scroll-overview 的基础上长出来的 fork,上游代码和历史都保留在仓库里 —— 包括那个 480KB 的 scrollOverview.cpp。两个项目之间没有隶属或背书关系。

为什么值得一看

它最值得看的地方不是「无限画布」这个点子 —— 这个点子本身不新 —— 而是把点子落地成了什么:用自己写的 GLSL 兑现镜片效果、把多屏的边界情况一条条写成规则再一条条挂上测试、给系统主题做 icon 回退、把调参结果隔离到单独文件、在不支持的行为上选择「安全地什么都不做」并说明理由。这些东西都不会出现在截图里,但决定了它能不能真的每天用。

顺带一提,9 月 26 日刚发布的 phantomat 同期还有一位邻居:同一个 Omarchy 生态里的 omarchy-meeting-recorder,把麦克风和系统声音录成两条独立的轨再转写。看得出来这个围绕 Omarchy 的小工具生态正在快速长出来。

封面图按项目自己的视觉语言生成:桶形镜片变形与暗角直接复刻自 shaders/barrel.frag 的公式。项目本身不打包任何图标资源,所以没有可用的官方 logo。

© 版权声明
THE END
喜欢就支持一下吧
点赞6 分享