如果你和我一样,把平铺窗口管理器当主力桌面用了好几年,肯定有过这种感觉:窗口越开越多、布局越来越乱,最后只能靠不断切换 tag 来维持秩序。我最初用的是 i3,后来换到 herbstluftwm,折腾过程中被一个词卡了很久——hyperframe。最初看手册时我根本没在意它,觉得无非就是“多层的 frame”而已。直到某天我试图在一个 frame 里嵌套出更复杂的布局时,才发现这个概念不搞清楚,后面的操作全是撞大运。
严格来说,hyperframe 不是某个独立软件,也不是什么新式窗口特效,它是 herbstluftwm 这类基于树形布局的平铺窗口管理器里的一个基础结构。简单说,当一个 frame 内部还包含着其他 frame,而不是只装窗口时,这个“父 frame”就是一个 hyperframe。它决定了你能否在平铺桌面里搭建出“主编辑区在左、右侧上下分栏”这种复杂布局,也决定了你在调整大小时为什么有时候会“整个布局一起变”。
这篇内容不是抄手册,是我在 herbstluftwm 里实打实配置、拆解、踩坑之后整理出来的。适合已经在用或准备尝试平铺窗口管理器的人,尤其是那些不满足于默认排版、想按自己习惯定制桌面布局的人。我会从原理讲起,再给出一套可以直接抄的实操方案,最后把我踩过的坑和排查思路一并交代清楚。
1. 为什么会有 hyperframe:从一个让我头疼的布局说起
1.1 最简单的一层平铺
平铺窗口管理器的核心思想很简单:窗口不再层层堆叠,而是像瓷砖一样铺满整个屏幕,每个窗口占据一块独立区域,没有重叠,也没有被遮挡的问题。这种做法的好处是省去了反复拖拽窗口、调整层级的麻烦,配合键盘切换窗口非常高效。
但“平铺”也分层次。很多人第一次接触 i3 时,会习惯性地把它理解成“上下左右分割屏幕”:按一下快捷键,当前窗口缩小,腾出来的空间被下一个窗口填满。这种理解在只有一层分割时是没问题的,窗口数量少、结构简单,怎么切都清楚。可一旦窗口多起来,或者你想手动控制多个区域的比例,问题就出现了。
我在 herbstluftwm 里第一次搭“编辑器在左侧、右侧分成上下两个终端”的布局时,下意识觉得只要连续按两次分割快捷键就行。结果发现屏幕上的关系根本不按我设想的走,所有新窗口都挤在同一层里,左右方向不断重复切分,整个布局乱成一团。
这说明我缺少一个关键认知:真正要控制的不是一个“平面”,而是一棵“树”。
1.2 frame、leaf 与 hyperframe:把窗口树讲清楚
在 herbstluftwm 里,所有的平铺区域都被组织成一棵树。树上的每个节点,要么是一个“叶子”,也就是一个实际显示的窗口;要么是一个内部节点,用来容纳子树,这个内部节点就叫 frame。一个 frame 可以包含多个子 frame,也可以包含多个窗口叶子,关键看它内部还有没有下一层结构。
于是就有了 hyperframe 的定义:当一个 frame 至少有一个子节点也是 frame 时,这个父 frame 就是 hyperframe。换句话说,普通 frame 是“直接装窗口的容器”,而 hyperframe 是“装着其他容器的容器”。
我用一个生活化的类比来理解这件事。普通 frame 就像书架上的一排格子,每格里放一本书,清晰明了。hyperframe 则像一个大抽屉,抽屉里面又隔成几小格,小格里再放书。你要找书的时候,先打开抽屉,再选择小格,最后拿到书。这个“抽屉套格子”的结构,就是 hyperframe 带来的层级。
这个定义看起来简单,但它在实际操作中的影响非常大。因为当你对一个普通 frame 执行 split 命令时,原 frame 会变成 hyperframe,原本的窗口会落到某个子 frame 中,而新产生的空白区域会成为另一个子 frame。如果你不理解这个“身份转变”,你很难预判焦点会跑到哪里,也很难解释为什么布局的层级关系变了。
1.3 为什么不能只开一级嵌套
有人可能会问:我能不能一直保持单层平铺,不做嵌套?可以,很多平铺窗口管理器默认就是这样,窗口多了就继续在同层内压缩。但这样做有两个绕不开的痛点。
第一个痛点是比例控制。同一层内窗口多了以后,每个窗口的平均宽度会越来越小。在宽屏显示器上,如果一个长期写作的编辑器和两个终端并排挤在同一行,编辑器可能只剩不到三分之一屏宽,根本没法用。你当然可以手动调整相邻窗口的比例,但调整的也只是“左右分界”这一条边,没法做到“一个区域占七成、区域内再上下分两栏各占五成”这种精细结构。
第二个痛点是窗口归类。实际工作场景里,窗口之间是有逻辑关系的。浏览器和文档可能属于“资料区”,编辑器和构建终端属于“开发区”,如果所有窗口都平铺在同一层,快捷键在它们之间切换时,焦点会按树形结构顺序走,无法做到“先在开发区内切换,再整体跳到资料区”这种分组操作。hyperframe 天然提供了这种分组能力,树形结构本身就是一个天然的导航地图。
我真正意识到这一点,是在一次长文写作任务中。我需要同时打开参考资料、Markdown 编辑器、终端,以及一个用来预览渲染结果的浏览器。如果只是简单平铺,四个窗口挤成一排,每个都很窄。后来我搭了一个三层结构:左侧编辑器占六成,右侧是一个 vertical 排列的 hyperframe,里面再分成上下两半,上半放参考资料,下半用终端和预览窗口。这样每个区域都有足够宽度,而且焦点切换可以按“区块”跳,用起来非常顺手。
2. 布局树与焦点:操作 hyperframe 前必须补的三块知识
2.1 一棵树从根 frame 长出来
在 herbstluftwm 里,每个 tag 都拥有一棵独立布局树。树的顶部是一个根 frame,它占据了整个屏幕。根 frame 可能直接包含若干窗口叶子,也可能先分出几个子 frame,再由子 frame 去容纳窗口。
当你同时使用多个显示器时,每个显示器上运行的 tag 也有各自的根 frame。这意味着不同显示器之间可以有不同的布局结构,互不干扰。你完全可以在左显示器上用水平分割的简单布局,在右显示器上搭一个复杂的嵌套 hyperframe。
理解这棵树是理解所有操作的前提。因为 herbstluftwm 里的 focus、shift、resize、split 等命令,本质上都是在这棵树上做节点操作,而不是在“屏幕坐标”上做算术。你按“向左移动焦点”时,系统寻找的是布局树中当前 frame 左侧的相邻 frame,而不是简单地按屏幕像素往左挪一个窗口。
我刚开始没意识到这一点,经常在 vertical 布局下按左右方向键,发现焦点纹丝不动,还以为是快捷键绑错了。后来才明白,在这个布局里左右方向根本没有相邻 frame,焦点自然无处可去。
2.2 frame 索引与路径
在 herbstluftwm 里,每个 frame 在 tag 内都有一个索引路径,格式类似0.1.2。第一个数字表示根 frame 下的第几个子节点,第二个数字表示往下再第几个子节点,依次类推。路径越长,说明这个 frame 在树中的位置越深。
查看当前布局可以使用herbstclient layout命令。它会输出一串用括号表示的结构,比如:
(root (horizontal (vertical (leaf)) (leaf)) (leaf))这个输出看起来有点吓人,但拆开读并不难:根 frame 采用 horizontal 方向,分成两个子树;第一个子 frame 是 vertical 方向,里面包含两个叶子;第二个子 frame 就是一个叶子。
路径概念最大的用处在于调试。当你觉得布局不对、焦点在奇怪的位置时,用herbstclient layout看一遍结构,比盯着屏幕猜要直观得多。我养成了一个习惯:每次搭建复杂布局前,先在空 tag 里逐步 split,然后每做一步就查看一次布局输出,确保这棵树是按我预期生长的。
2.3 焦点移动与 frame 指向的区别
很多刚接触 hyperframe 的人会混淆两个东西:焦点在“窗口”上,和焦点在“frame”上。
当焦点落在一个叶子窗口上时,你能直接输入内容。当焦点落在一个空白 frame 或 hyperframe 上时,屏幕上可能没有可交互的窗口,但这个 frame 本身是“激活”的,你这时启动一个新程序,它会自动出现在这个 frame 内;你执行 split 命令,也会作用在这个 frame 上。
这个区别特别容易造成“找不到窗口”的情况。比如我用方向键把焦点移到一个刚 split 出来的空 frame 上,屏幕显示一大片空白,我以为程序崩了。实际上程序还在,只是焦点所在的 frame 里还没有任何窗口,而树的其他部分被折叠或移出了可视区域。
解决这种状况有两个思路。一是用方向键继续移动焦点,让焦点回到有窗口的 frame;二是直接在当前空的 frame 里启动一个新程序,把它填上。如果你只是想删掉这个空白区域,可以执行herbstclient remove,但要注意,它会移除当前空 frame,如果 frame 里有窗口则不会执行成功。这个细节很重要,后面我会专门讲。
3. 实操:用 herbstluftwm 从零搭一套嵌套布局
3.1 先熟悉几个核心命令
在真正动手之前,先把我最常用的几个 herbstclient 命令列出来。这些命令是搭建 hyperframe 结构的基础,建议在空 tag 里先各跑一遍,感受一下效果。
herbstclient split right 0.5:把当前 frame 沿水平方向一分为二,原窗口保留在左侧,右侧产生一个新 frame,新 frame 占 50% 比例。方向参数可以是 left、right、top、bottom。herbstclient focus right:把焦点移动到右侧相邻 frame。方向键同理。herbstclient shift right:把当前窗口移动到右侧相邻 frame。如果右侧是 hyperframe,窗口会进入该 frame 的某个子位置。herbstclient resize right +0.05:调整当前 frame 与右侧 frame 的分界线位置,以当前 frame 尺寸的百分比为单位。herbstclient rotate:改变当前 frame 内子节点的排列方向,在 vertical 与 horizontal 之间切换。herbstclient merge right:把右侧相邻 frame 与当前 frame 合并,右侧的窗口会并入当前 frame。herbstclient remove:移除当前空 frame,并让父 frame 回收空间。
这些命令在 X11 环境下配合快捷键使用非常顺手。我习惯把 split、focus、shift 分别绑定到 Mod+方向键和 Mod+Ctrl+方向键上,日常操作完全不用碰鼠标。
3.2 第一次构造 hyperframe 的完整过程
我以一个具体目标为例:搭建“左侧编辑器占 70%,右侧上下分栏,上方是浏览器,下方是终端”的布局。
第一步,确保当前 tag 是空的,没有遗留布局。可以用herbstclient layout确认一下,若输出只有(root (leaf))就说明是干净状态。
第二步,在当前根 frame 里启动一个编辑器。此时布局是(root (leaf)),也就是一个普通 leaf。
第三步,执行herbstclient split right 0.7。这一步执行后,根 frame 变成了左右两个子 frame,原本的编辑器被分配到左侧的子 frame,右侧产生一个空的子 frame。此时根 frame 的身份已经变成了 hyperframe,因为它的两个子节点中至少有一个是 frame。
第四步,把焦点切换到右侧空 frame:herbstclient focus right。此时焦点落在一个空 frame 上,屏幕的右侧区域是空白的。
第五步,在右侧执行herbstclient split bottom 0.5。这一步会在右侧 frame 内部产生上下两个子 frame,右侧 frame 因此也变成了 hyperframe。随后把焦点移到下方:herbstclient focus down。
第六步,在下方空 frame 里启动终端:herbstclient spawn xterm。在上方启动浏览器,可以先把焦点移上去再 spawn。
完成之后,用herbstclient layout查看结构,你会看到一个嵌套了两层的树形输出。这个布局里,根 frame 是总控,左侧叶子是编辑器,右侧是一个子 hyperframe,内部再分出上下两个叶子。这就是 hyperframe 最常见的应用形态。
整个过程中最值得注意的点是:split 之后,原 frame 的身份自动从“装窗口的容器”变成“装容器的容器”,如果你没有意识到这个变化,后续焦点移动就会很混乱。
3.3 调整比例、旋转方向与合并拆分
搭建好结构之后,接下来的高频操作就是调比例。resize right +0.05的含义是把当前 frame 的右边界向右移动 5% 的幅度,相当于把左侧区域“变大一点”。同理,resize left -0.05可以调整左边界。
初学者最容易踩的坑是:以为 resize 是“把当前窗口变大”,方向参数也按这个理解来选。实际上 resize 的方向指的是“调整哪条边”,对象是当前 frame 与相邻 frame 之间的分界线。在 vertical 布局的 frame 内部,左右方向通常没有可调整的分界线;在 horizontal 布局里,上下方向的分界线才是关键。我建议刚开始时只绑定一个方向的测试键,比如resize left +0.05和resize right +0.05,在空 tag 里反复实验,把“边”和“方向”的对应关系搞明白再扩展到全方向。
rotate 命令也比较常用。它改变的是当前 frame 内部子节点的排列方向。如果你本来想让两个子 frame 左右排列,结果 split 时方向选错了,变成了上下排列,不需要重新拆,直接执行 rotate 就能翻转。
merge 和 remove 则是“拆掉”布局的路径。merge 会把相邻 frame 的窗口合并到当前 frame 里,常用于取消某层嵌套。remove 只能移除空 frame,如果 frame 里有窗口会拒绝执行。一个我常用的组合是:先 shift 把窗口从空 frame 移走,再 remove 掉这个空 frame,从而把布局树的一支彻底清理干净。
3.4 把常用操作固化成快捷键与脚本
手动敲命令太慢,真正好用的方案是把这些操作绑定成快捷键,再写一些常用布局的初始化脚本。
快捷键绑定示例:
herbstclient keybind Mod4+Return spawn xterm herbstclient keybind Mod4+Shift+Return spawn urxvt herbstclient keybind Mod4+Left focus left herbstclient keybind Mod4+Right focus right herbstclient keybind Mod4+Up focus up herbstclient keybind Mod4+Down focus down herbstclient keybind Mod4+Shift+Left shift left herbstclient keybind Mod4+Shift+Right shift right herbstclient keybind Mod4+s split right 0.5 herbstclient keybind Mod4+Shift+s split bottom 0.5 herbstclient keybind Mod4+r rotate herbstclient keybind Mod4+m merge right herbstclient keybind Mod4+x remove把上面这些绑定写进 autostart 文件后,每次启动都能把桌面调整成顺手的状态。更重要的是,如果你想一键构建某个复杂布局,可以把前面 3.2 节的步骤写成一个 shell 函数。比如:
mysetup() { herbstclient chain \ , split right 0.7 \ , focus right \ , split bottom 0.5 \ , focus down \ , spawn xterm \ , focus up \ , spawn xterm }执行 mysetup 之后,当前 tag 会直接变成左边编辑器、右侧上浏览器、右侧下终端的布局。注意这里在右侧下方 spawn 的 xterm 只是示例,你可以替换成自己常用的程序。这个脚本的核心价值在于:它把“先建树,再启动程序”的顺序固化下来,避免手动操作时因为启动顺序不同造成窗口跑错位置。
4. 多显示器与动态场景:hyperframe 的进阶玩法
4.1 monitor 与独立布局树
如果你的机器接了多个显示器,herbstluftwm 会给每个显示器分配一个相对独立的运行空间。每个显示器上的 tag 都有一棵独立的根 frame 布局树,这意味着你可以在不同的物理屏幕上使用完全不同的窗口组织结构。
我自己是双显示器用户,左屏用于代码编辑和阅读,右屏用于终端和日志监控。左屏搭的是一个大型 hyperframe,右侧分栏放文档和代码对比;右屏则是简单的上下分割。使用过程中,我只需要记住当前焦点在哪个显示器上,然后执行对应的 focus 和 shift 操作即可。
多显示器带来的最大问题是焦点跳转的“方向感”。当两个显示器并排放置时,把一个显示器最右侧的窗口往右移,可能会进入另一个显示器最左侧的 frame,也可能不会,这取决于布局树中相邻 frame 的判定。解决方法是多观察herbstclient layout的输出,别凭屏幕直觉判断。我见过不少人在双屏下把窗口 shift 到“看不见的地方”,其实就是布局树的相邻关系和自己想象的场景不一致。
4.2 浮动层与全屏的取舍
hyperframe 只管理平铺窗口,浮动窗口是另一套机制。herbstluftwm 允许你把某些窗口设为浮动状态,它们会悬浮在平铺布局之上,不参与 frame 树的尺寸分配。这有利有弊。
好处是,像弹出对话框、取色器这类临时窗口,浮动起来更自然,不会打断平铺布局。坏处是,如果你把一个大窗口设为浮动,它会遮住底下的 hyperframe 结构,视觉上造成“布局坏了”的假象。我遇到过一次:某程序主窗口默认浮动,启动后半屏挡住了整个右侧 hyperframe,我以为是布局树崩溃,查了半天,其实是浮动层的层级问题。
我的经验是:日常开发程序都保持平铺,只有确实需要置顶的工具类窗口才浮动。全屏窗口则独立处理,它是一种特殊的显示状态,会暂时盖住整个 frame 树,退出全屏后布局恢复原样。
4.3 动态重排:用脚本模拟“窗口布局模板”
hyperframe 真正的威力在于,你可以针对不同工作场景预定义多种布局模板,需要时一键切换到对应 tag 再启动程序。
比如“写作模式”模板:左侧是编辑器,右侧上方是参考资料,右上角是预览,下方是终端。“开发模式”模板:左侧是代码编辑器,右侧分成上下两半,上半是浏览器测试窗口,下半是调试终端。“运维模式”模板:整屏分成四个等大的 frame,分别运行不同的日志监控工具。
实现思路很简单:把 3.4 节的 mysetup 函数扩展出多个变体,分别绑定到不同快捷键上。注意每次切换前先清空当前 tag 的布局,可以用herbstclient remove逐层清理,或者直接把当前 tag 切到另一个空 tag,再从干净状态开始搭建。
我之前担心动态切换会破坏已有窗口的位置,后来发现一个很实用的技巧:在切换前先把当前 tag 里所有窗口用herbstclient shift移到一个备用 tag 中,等需要时再从备用 tag 移回来。这样既保住了窗口,又不会干扰新布局的构造。
5. 常见问题与排查技巧实录
5.1 问题速查表
| 现象 | 可能原因 | 解决思路 |
|---|---|---|
| 焦点移动后屏幕空白 | 焦点进入了一个没有窗口的 frame | 直接在当前 frame 启动程序,或继续移动焦点 |
| resize 命令没有效果 | 方向与当前 frame 布局不匹配 | 查看 layout 结构,尝试反方向或切换 rotate |
| 布局在 reload 后乱了 | 窗口启动顺序和布局构建顺序不一致 | 把建树和启动写在同一个脚本里,固定执行顺序 |
| 新窗口出现在错误位置 | 焦点不在期望的 frame 上 | 先执行 focus 确认焦点,再 spawn 程序 |
| remove 无法删除 frame | frame 内仍存在窗口 | 先 shift 移走窗口,再执行 remove |
| 同一个 frame 里窗口挤在一起 | 没有创建新的子 frame,只在同层平铺 | 先 split,再在新 frame 中放窗口 |
5.2 焦点卡在空白 frame 里,怎么把它“赶出去”
这个问题我遇到得最频繁。当你把一个 frame 里的窗口移走,或者刚 split 完还没来得及启动程序,焦点就会落在一个空 frame 上。此时如果你按方向键,可能发现焦点并不移动,因为布局树的相邻关系并不总是和屏幕方向一致,尤其是当空 frame 被夹在嵌套结构内部时。
解决办法分几种。最简单的,直接在这个空 frame 里启动一个新程序,把它利用起来。如果这个空 frame 的存在本来就是操作失误造成的,就执行herbstclient remove把它删掉。还有一个更灵活的方式:用herbstclient focus配合特定参数跳转到最近的窗口,但不同版本的 herbstluftwm 语法略有差异,我建议优先用前两种方式,简单可靠。
5.3 resize 不动或方向总反,多半是理解错了参照系
有一段时间我特别困惑,为什么我在 horizontal 布局下执行resize right +0.05,左侧窗口确实变大了,但右侧窗口好像没变化,有时整个布局还会跳一下。后来我意识到,resize 修改的是“边界线”的位置,而边界线的移动会影响两侧 frame 的实际尺寸,但百分比是相对于当前 frame 的父 frame 尺寸计算的。所以你在不同层级执行同样的命令,实际变化幅度可能不一样。
我在实际使用中摸索出一个技巧:把 resize 的增量设小一点,多执行几次,观察效果。比如先用resize right +0.01试一次,确认方向对了,再执行resize right +0.05完成调整。如果需要更大范围调整,就连续执行多次。这个方法虽然笨,但能避免一次调整过头导致布局比例失控。
5.4 布局在 reload 后“变形”的真实原因
herbstluftwm 的 reload 机制会重新加载 autostart 脚本,但不会主动重放你在运行时手动执行过的所有命令。如果你在运行时手动 split 了一个复杂布局,然后执行 reload,布局树本身是有持久化机制的,理论上可以恢复。真正会出问题的,是你启动的应用程序顺序。
假设你的 autostart 里直接写了三条 spawn 命令,系统启动时,这三个窗口会按顺序进入当前 tag 的焦点 frame。如果你之前手动调整过布局树,窗口可能没有按预期进入对应位置。解决办法是把布局构建命令和启动命令放在同一个脚本块中,确保先建树、再启动,或者用herbstclient chain串联执行,避免时序竞争。
我自己还踩过一个细节坑:某些程序启动时会短暂抢占焦点,导致后续命令作用在错误的目标 frame 上。这种情况下,可以给每个 spawn 前加一个短暂的sleep 0.1,或者用守护方式等待窗口映射完成后再继续执行下一步。虽然不优雅,但实测能解决大部分问题。
5.5 一些小技巧的总结
- 用
herbstclient layout做所有调试的第一步,先把树结构看明白,不要凭感觉操作。 - 在 autostart 里固定写好常用布局模板,比每次手动 split 高效得多。
- 光标在空白 frame 时,不要慌,先 spawn 一个终端确认焦点归属。
- 大胆使用 merge 和 remove,布局树是可以反复重构的,不要怕拆坏。
- 多显示器环境下,注意每个显示器的布局树相互独立,焦点操作要结合 monitor 判断。
我在实际使用中发现,hyperframe 真正提升效率的时刻,不是搭建好布局的那一瞬间,而是后续几个月里,你可以随时用快捷键把窗口精确移动到树中的指定位置,整个过程不用鼠标、不用反复调整。那种“桌面完全听你指挥”的感觉,才是它最有价值的回报。如果你刚接触这个概念,建议先在空 tag 里随便折腾,split、rotate、merge、remove 全部试一遍,把树形结构这个底层思维建立起来,之后所有操作都会顺很多。