赞美 grobi:自动配置 X11 显示器
摘要
本文赞扬了 grobi,一款用于 X11 上自动配置显示器的工具,描述了它如何简化双 DP 8K 显示器设置的管理,并与 autorandr 等替代方案进行了比较。
<p>我最近重新开始使用 Alexander Neumann 的 <a href="https://github.com/fd0/grobi/"><code>grobi</code> 程序</a>,并欣喜地发现,它让我那台麻烦(但很棒的)<a href="/posts/2017-12-11-dell-up3218k/">戴尔 32 英寸 8K 显示器 (UP3218K)</a> 使用起来更加方便——比之前基于休眠的方法更快获得信号。</p>
<p>之前,当我的电脑从挂起到内存中唤醒时,有两种情况:</p>
<ol>
<li>显示器已连接。我的 <a href="#zleep">sleep 程序</a> 会打开显示器电源(如果需要),稍作休眠,然后运行 <a href="https://manpages.debian.org/xrandr.1"><code>xrandr(1)</code></a> 以(希望)正确配置显示器。</li>
<li>显示器未连接,例如因为它仍连接到我的工作电脑。</li>
</ol>
<p>在场景 ② 中,或者如果场景 ① 中的一次性配置尝试失败,我需要从另一台计算机通过 SSH 登录并手动运行 <code>xrandr</code>,以便显示器显示信号:</p>
<pre tabindex="0"><code>% DISPLAY=:0 xrandr \
--output DP-4 --mode 3840x4320 --panning 0x0+0+0 \
--output DP-2 --right-of DP-4 --mode 3840x4320 --panning 0x0+3840+0
</code></pre><h2 id="automatic-monitor-configuration-with-grobi">使用 grobi 自动配置显示器</h2>
<p>我现在通过创建以下 <code>~/.config/grobi.conf</code> 文件完全解决了这个问题:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#062873;font-weight:bold">rules</span>:<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"> </span>- <span style="color:#062873;font-weight:bold">name</span>:<span style="color:#bbb"> </span>UP3218K<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"> </span><span style="color:#062873;font-weight:bold">outputs_connected</span>:<span style="color:#bbb"> </span>[DP-2, DP-4]<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"> </span><span style="color:#60a0b0;font-style:italic"># DP-4 在左,DP-2 在右</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"> </span><span style="color:#062873;font-weight:bold">configure_row</span>:<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"> </span>- DP-4@3840x4320<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"> </span>- DP-2@3840x4320<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"> </span><span style="color:#60a0b0;font-style:italic"># atomic 指示 grobi 只调用一次 xrandr 并配置所有输出。这并不总能适用于所有显卡,但为了成功配置 UP3218K 显示器是必需的。</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"> </span><span style="color:#062873;font-weight:bold">atomic</span>:<span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">true</span><span style="color:#bbb">
</span></span></span></code></pre></div><p>……并在 Arch Linux 上安装/启用 <code>grobi</code>:</p>
<pre tabindex="0"><code>% sudo pacman -S grobi
% systemctl --user enable --now grobi
</code></pre><p>每当 <code>grobi</code> 检测到我的显示器已连接(它监听 <a href="https://cgit.freedesktop.org/xorg/proto/randrproto/tree/randrproto.txt">X11 RandR</a> 输出变化事件),它就会运行 <a href="https://manpages.debian.org/xrandr.1"><code>xrandr(1)</code></a> 来配置显示器的分辨率和位置。</p>
<p>要检查 <code>grobi</code> 看到了什么/做了什么,可以使用:</p>
<pre tabindex="0"><code>% systemctl --user status grobi
% journalctl --user -u grob
</code></pre><p>例如,在我的系统上,我看到:</p>
<pre tabindex="0"><code>grobi: 18:31:48.823765 outputs: [HDMI-0 (primary) DP-0 DP-1 DP-2 (connected) 3840x2160+ [DEL-16711-808727372-DELL UP3218K-D2HP805I043L] DP-3 DP-4 (connected) 3840x21>
grobi: 18:31:48.823783 new rule found: UP3218K
grobi: 18:31:48.823785 enable outputs: [DP-4@3840x4320 DP-2@3840x4320]
grobi: 18:31:48.823789 using one atomic call to xrandr
grobi: 18:31:48.823806 running command /usr/bin/xrandr xrandr --output DP-4 --mode 3840x4320 --output DP-2 --mode 3840x4320 --right-of DP-4
grobi: 18:31:49.285944 new RANDR change event received
</code></pre><p>值得注意的是,现在从无信号状态恢复的步骤是关闭显示器电源再重新打开。这会触发 RandR 输出变化事件,进而触发 <code>grobi</code> 运行 <code>xrandr</code>,从而配置显示器。很好!</p>
<h2 id="why-not-autorandr">为什么不用 autorandr?</h2>
<p>没有特别的原因。我知道 <code>grobi</code>。</p>
<p>至少 <code>grobi</code> 是用 Go 编写的,因此它很可能在未来几年内保持稳定工作。</p>
<h2 id="does-grobi-work-on-wayland">grobi 能在 Wayland 上工作吗?</h2>
<p>很可能不行。<a href="https://github.com/fd0/grobi/">grobi 仓库</a>中没有提及 Wayland。</p>
<h2 id="zleep">附加内容:我的挂起到内存设置</h2>
<p>作为附加内容,本节描述了与显示器相关的自动化的另一半。</p>
<p>当我把电脑挂起到内存时,我要么稍后手动唤醒它(例如按键盘上的键或发送 Wake-on-LAN 数据包),要么希望它在每天早上 6:50 自动唤醒——这样,在我开始使用电脑之前,每日的 cron 任务就有时间运行。</p>
<p>为此,我使用 <code>zleep</code>,一个围绕 <a href="https://manpages.debian.org/rtcwake.8"><code>rtcwake(8)</code></a> 和 <code>systemctl suspend</code> 的包装程序,它与 myStrom 开关智能插头集成,可以完全关闭显示器电源。这很值得,因为显示器即使在待机状态下也消耗 30W 的功率!</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">package</span><span style="color:#bbb"> </span>main<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"></span><span style="color:#007020;font-weight:bold">import</span><span style="color:#bbb"> </span>(<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"> </span><span style="color:#4070a0">"context"</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"> </span><span style="color:#4070a0">"flag"</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"> </span><span style="color:#4070a0">"fmt"</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"> </span><span style="color:#4070a0">"bb</span></span></span></code></pre></div>
查看缓存全文
缓存时间: 2026/05/16 03:34
# 赞美 grobi 自动配置 X11 显示器
来源:https://michael.stapelberg.ch/posts/2025-05-10-grobi-x11-monitor-autoconfig/
目录
- 使用 grobi 自动配置显示器 {#automatic-monitor-configuration-with-grobi}
- 为什么不使用 autorandr? {#why-not-autorandr}
- grobi 能在 Wayland 上工作吗? {#does-grobi-work-on-wayland}
- 附加内容:我的挂起到内存设置 {#zleep}
我最近重新开始使用 Alexander Neumann (https://github.com/fd0/grobi/) 开发的 `grobi` 程序,并惊喜地发现它让我那台挑剔但绝佳的 Dell 32 英寸 8K 显示器 (UP3218K) (https://michael.stapelberg.ch/posts/2017-12-11-dell-up3218k/) 使用起来更加方便——相比之前依赖睡眠的方案,我现在能更快地获得信号。
以前,当我的 PC 从挂起到内存状态唤醒时,会出现两种情况:
1. 显示器已连接。我的睡眠程序 (https://michael.stapelberg.ch/posts/2025-05-10-grobi-x11-monitor-autoconfig/#zleep) 会打开显示器(如果需要)、稍等片刻,然后运行 `xrandr(1)` (https://manpages.debian.org/xrandr.1) (希望如此)正确配置显示器。
2. 显示器未连接,例如它仍然连接着我的工作 PC。
在情况 2,或者情况 1 中一次性配置尝试失败时,我需要从另一台电脑通过 SSH 登录,手动运行 `xrandr` 以便显示器显示信号:
```
% DISPLAY=:0 xrandr \
--output DP-4 --mode 3840x4320 --panning 0x0+0+0 \
--output DP-2 --right-of DP-4 --mode 3840x4320 --panning 0x0+3840+0
```
## 使用 grobi 自动配置显示器 {#automatic-monitor-configuration-with-grobi}
我现在通过创建以下 `~/.config/grobi.conf` 文件完全解决了这个问题:
```
rules:
- name: UP3218K
outputs_connected: [DP-2, DP-4]
# DP-4 在左,DP-2 在右
configure_row:
- DP-4@3840x4320
- DP-2@3840x4320
# atomic 指示 grobi 只调用一次 xrandr 并配置所有输出。
# 这并非在所有显卡上都有效,但对于成功配置 UP3218K 显示器是必需的。
atomic: true
```
...并通过以下方式安装/启用 `grobi`(在 Arch Linux 上):
```
% sudo pacman -S grobi
% systemctl --user enable --now grobi
```
每当 `grobi` 检测到我的显示器已连接(它监听 X11 RandR (https://cgit.freedesktop.org/xorg/proto/randrproto/tree/randrproto.txt) 输出变化事件),它就会运行 `xrandr(1)` (https://manpages.debian.org/xrandr.1) 来配置显示器的分辨率和位置。
要查看 `grobi` 正在看到/做什么,你可以使用:
```
% systemctl --user status grobi
% journalctl --user -u grobi
```
例如,在我的系统上,我看到:
```
grobi: 18:31:48.823765 outputs: [HDMI-0 (primary) DP-0 DP-1 DP-2 (connected) 3840x2160+ [DEL-16711-808727372-DELL UP3218K-D2HP805I043L] DP-3 DP-4 (connected) 3840x21>
grobi: 18:31:48.823783 new rule found: UP3218K
grobi: 18:31:48.823785 enable outputs: [DP-4@3840x4320 DP-2@3840x4320]
grobi: 18:31:48.823789 using one atomic call to xrandr
grobi: 18:31:48.823806 running command /usr/bin/xrandr xrandr --output DP-4 --mode 3840x4320 --output DP-2 --mode 3840x4320 --right-of DP-4
grobi: 18:31:49.285944 new RANDR change event received
```
值得注意的是,现在从不良状态(无信号)恢复的指示是关闭显示器电源再重新打开。这将导致 RandR 输出变化事件,进而触发 `grobi`,运行 `xrandr`,配置显示器。太棒了!
## 为什么不使用 autorandr? {#why-not-autorandr}
没有特别的原因。我知道 `grobi`。
至少 `grobi` 是用 Go 编写的,因此它很可能在未来几年内保持平稳运行。
## grobi 能在 Wayland 上工作吗? {#does-grobi-work-on-wayland}
很可能不行。grobi 仓库 (https://github.com/fd0/grobi/) 中没有提到 Wayland。
## 附加内容:我的挂起到内存设置 {#zleep}
作为附加内容,本节描述了我显示器相关自动化的另一半。
当我把 PC 挂起到内存时,我希望要么稍后手动唤醒它——例如按下键盘上的按键或发送 Wake-on-LAN 数据包——要么让它每天早上 6:50 自动唤醒——这样,在我开始使用电脑之前,每日 cron 作业就有一些时间运行。
为了实现这一点,我使用了 `zleep`,一个围绕 `rtcwake(8)` (https://manpages.debian.org/rtcwake.8) 和 `systemctl suspend` 的封装程序,并与 myStrom 开关智能插头集成,以完全关闭显示器的电源。这样做是值得的,因为显示器即使在待机状态下也会消耗 30W 的功率!
```
package main
import (
"context"
"flag"
"fmt"
"log"
"net/http"
"net/url"
"os"
"os/exec"
"time"
)
var (
resume = flag.Bool("resume",
false,
"run resume behavior only (turn on monitor via smart plug)")
noMonitor = flag.Bool("no_monitor",
false,
"disable turning off/on monitor")
)
func monitorPower(ctx context.Context, method, cmnd string) error {
if *noMonitor {
log.Printf("[monitor power] skipping because -no_monitor flag is set")
return nil
}
log.Printf("[monitor power] command: %v", cmnd)
u, err := url.Parse("http://myStrom-Switch-A46FD0/" + cmnd)
if err != nil {
return err
}
for {
if err := ctx.Err(); err != nil {
return err
}
req, err := http.NewRequest(method, u.String(), nil)
if err != nil {
return err
}
ctx, canc := context.WithTimeout(ctx, 5*time.Second)
defer canc()
req = req.WithContext(ctx)
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Print(err)
time.Sleep(1 * time.Second)
continue
}
if resp.StatusCode != http.StatusOK {
log.Printf("unexpected HTTP status code: got %v, want %v", resp.Status, http.StatusOK)
time.Sleep(1 * time.Second)
continue
}
log.Printf("[monitor power] request succeeded")
return nil
}
}
func nextWakeup(now time.Time) time.Time {
midnight := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
if now.Hour() < 6 {
// wake up today
return midnight.Add(6*time.Hour + 50*time.Minute)
}
// wake up tomorrow
return midnight.Add(24 * time.Hour).Add(6*time.Hour + 50*time.Minute)
}
func runResume() error {
// Retry for up to one minute to give the network some time to come up
ctx, canc := context.WithTimeout(context.Background(), 1*time.Minute)
defer canc()
if err := monitorPower(ctx, "GET", "relay?state=1"); err != nil {
log.Print(err)
}
return nil
}
func zleep() error {
ctx := context.Background()
now := time.Now().Truncate(1 * time.Second)
wakeup := nextWakeup(now)
log.Printf("now : %v", now)
log.Printf("wakeup: %v", wakeup)
log.Printf("wakeup: %v (timestamp)", wakeup.Unix())
// assumes hwclock is running in UTC (see timedatectl | grep local)
// Power the monitor off in 15 seconds.
// mode=on is intentional: https://api.mystrom.ch/#e532f952-36ea-40fb-a180-a57b835f550e
// - the switch will be turned on (already on, so this is a no-op)
// - the switch will wait for 15 seconds
// - the switch will be turned off
if err := monitorPower(ctx, "POST", "timer?mode=on&time=15"); err != nil {
log.Print(err)
}
sleep := exec.Command("sh", "-c", fmt.Sprintf("sudo rtcwake -m no --verbose --utc -t %v && sudo systemctl suspend", wakeup.Unix()))
sleep.Stdout = os.Stdout
sleep.Stderr = os.Stderr
fmt.Printf("running %v\n", sleep.Args)
if err := sleep.Run(); err != nil {
return fmt.Errorf("%v: %v", sleep.Args, err)
}
return nil
}
func main() {
flag.Parse()
if *resume {
if err := runResume(); err != nil {
log.Fatal(err)
}
} else {
if err := zsleep(); err != nil {
log.Fatal(err)
}
}
}
```
为了在恢复后打开显示器的电源,我在 `/lib/systemd/system-sleep/zleep.sh` 中放置了以下 shell 脚本:
```
#!/bin/sh
case "$1" in
pre) exit 0
;;
post) /usr/local/bin/zleep -resume
exit 0
;;
*) exit 1
;;
esac
```
一旦电源打开,grobi 就会检测并配置显示器。
以下是该程序的实际运行情况:
```
2025/05/06 21:58:32 now : 2025-05-06 21:58:32 +0200 CEST
2025/05/06 21:58:32 wakeup: 2025-05-07 06:50:00 +0200 CEST
2025/05/06 21:58:32 wakeup: 1746593400 (timestamp)
2025/05/06 21:58:32 [monitor power] command: timer?mode=on&time=15
2025/05/06 21:58:32 [monitor power] request succeeded
running [sh -c sudo rtcwake -m no --verbose --utc -t 1746593400 && sudo systemctl suspend]
Using UTC time.
delta = 0
tzone = 0
tzname = UTC
systime = 1746561512, (UTC) Tue May 6 19:58:32 2025
rtctime = 1746561512, (UTC) Tue May 6 19:58:32 2025
alarm 1746593400, sys_time 1746561512, rtc_time 1746561512, seconds 0
rtcwake: wakeup using /dev/rtc0 at Wed May 7 04:50:00 2025
suspend mode: no; leaving
```
喜欢这篇文章吗?订阅本博客的 RSS 源 (https://michael.stapelberg.ch/feed.xml) 以免错过任何新文章!
我从 2005 年开始运行这个博客,分享知识和经验已经超过 20 年了!:)
相似文章
介绍 Grok Build(2 分钟阅读)
Grok Build 是一款从终端运行的新编码代理,目前面向 SuperGrok Heavy 订阅者提供早期测试版。它支持插件、子代理和深度工作树集成,包括用于脚本编写的无头模式。
@Dell:用 Dell UltraSharp 32 4K QD-OLED 显示器,以全新视角审视你的工作空间。Monitor Nerds 深入剖析了它为何……
Dell UltraSharp 32 4K QD-OLED 显示器(U3226Q)带来影院级对比度并内置自动校色,成为创意工作与娱乐的旗舰之选。
我的CLI现在可以控制整个桌面,有什么好的测试可以验证它是否真正有效?
用户描述了一个通过混合使用鼠标、键盘和截图方式控制整个桌面的CLI工具,该工具成功完成了发送电子邮件截图和远程桌面控制等任务。用户希望寻找具有挑战性的测试来验证其稳健性。
Grok 连接器
X AI 推出 Grok Connectors,允许用户将日常应用程序直接集成到 Grok AI 平台中。
Gretl
Gretl 是一款专为管理本地主机环境而设计的可视化控制面板。