- 桌面应用
- 操作系统
【免费下载链接】qtile
:cookie: A full-featured, hackable tiling window manager written and configured in Python (X11 + Wayland)
Qtile 在 X11 与 Wayland 两大后端中原生支持空闲(idle)事件机制:你可以在系统持续无输入达到指定时长后触发自定义动作(如熄灭/调暗屏幕),也可以定义规则在特定窗口(如全屏播放视频)存在时抑制这些定时器,避免显示器被无故关闭。读完本文,你将掌握idle_timers与idle_inhibitors两个配置段的完整用法、每个参数的含义与默认值,并了解这套机制在源码层面(IdleNotifier、IdleInhibitorManager)以及两个后端中的具体工作原理。
一、总览:Qtile 空闲事件是什么
空闲事件系统解决一个很常见的桌面场景问题:"多久没有操作就执行某个动作,以及在什么情况下不要执行"。Qtile 将这一能力拆成了两个互补的组件:
- Idle timers(空闲定时器):当系统持续空闲达到设定的秒数后,执行
action;当检测到用户再次输入时,执行resume(恢复动作)。 - Idle inhibitors(空闲抑制器):为某些窗口定义规则,只要规则命中且窗口处于指定状态,定时器的动作就不会触发(例如全屏观看视频时禁止熄屏)。
这套机制被设计成后端无关(backend-agnostic):X11 和 Wayland 后端各自实现了底层的事件监听,而上层配置接口完全一致,用户在config.py里写的代码可以原样在两个后端下运行。
Wayland 协议的额外加成:Wayland 后端实现了ext_idle_notifier_v1与zwp_idle_inhibit_manager_v1两个协议(对应实现见 wayland/qw/server.c 中对 idle timers / inhibitors 的管理),因此像swayidle这样的标准 Wayland 客户端可以直接与 Qtile 协作:swayidle可以利用 Qtile 暴露的 idle notifier 信息来触发自己的动作,而客户端也可以通过zwp_idle_inhibit_manager_v1主动向 Qtile 请求抑制空闲状态。
注意(来自官方文档的语义细节):
- 如果一个抑制器阻止了定时器触发,那么即使抑制器随后被移除,只要系统仍处于空闲状态,该定时器的
action也不会补发。- 同理,如果在超时完成的那一刻系统里存在激活的抑制器,那么退出空闲状态时对应的
resume动作也不会触发。从源码看,这个语义由 IdleNotifier.fire_action() 实现:只有当
not (self.core.inhibited and timer.respect_inhibitor)时动作才会被执行,且fire_resume()只对fired == True的定时器生效(fire_resume())。
二、Idle timers:配置空闲定时器
2.1 配置位置与格式
定时器在配置文件的idle_timers段中定义,它是一个IdleTimer对象的列表。默认配置(resources/default_config.py)中该值为空列表[],即默认不启用任何空闲动作。
from libqtile.config import IdleTimer from libqtile.lazy import lazy idle_timers = [ IdleTimer(300, action=lazy.spawn("/path/to/screen_dimmer.sh"), resume=lazy.spawn("/path/to/restore_screen.sh")), IdleTimer(900, action=lazy.spawn("/path/to/screen_off.sh")) ]这是官方文档给出的标准示例,含义很直观:
- 空闲300 秒(5 分钟):调用
screen_dimmer.sh调暗屏幕;一旦检测到用户输入,立即调用restore_screen.sh恢复屏幕。 - 空闲900 秒(15 分钟):调用
screen_off.sh关闭屏幕(没有配置resume,恢复工作交给系统自身的唤醒流程)。
2.2 IdleTimer 构造参数详解
根据 config.py 中 IdleTimer 的定义,IdleTimer共接受四个参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
timeout | int | 必填 | 空闲超时秒数。必须是非负整数,否则抛出ValueError(timeout < 0时报Invalid idle timeout specified) |
action | callable /LazyCall/ 协程 | None | 达到超时时执行的动作(可选) |
resume | callable /LazyCall/ 协程 | None | 检测到用户输入时执行的动作(可选;仅在对应定时器已fired时触发) |
respect_inhibitor | bool | True | 当存在激活的抑制器时,是否仍然触发action。设为False可让该定时器无视抑制规则 |
同时注意两个硬性约束:
action与resume至少设置一个,否则构造时直接抛出ValueError("You must set one of 'action' or 'resume'.")。action和resume还支持协程(coroutine),此时它们会被异步执行。这一点在源码的 docstring 中有明确说明(config.py#L1187),对应的执行逻辑在 IdleNotifier._run_action():LazyCall通过 Qtile 的 command server 调用,协程函数用create_task调度,普通 callable 直接同步调用。
多个定时器的行为:定时器列表会被自动按 timeout 升序排序并去重(相同 timeout 只注册一个底层定时器),这一点由 IdleNotifier.add_timers() 的sorted(timers)实现,并在测试 test/backend/test_idle_notify.py#L61-L72 中验证:[2, 1, 2]的输入会归一化为[1, 2]。在 X11 后端,相邻两个 timeout 的差值会被拆成递增的中间定时器(见下文 X11 实现)。
2.3 实际应用:一个可落地的完整例子
把官方示例扩展成一段更完整的配置,可以同时包含"调暗 → 关屏 → 挂起"三级防呆,并演示respect_inhibitor的用法:
from libqtile.config import IdleTimer from libqtile.lazy import lazy idle_timers = [ # 3 分钟无操作:调暗屏幕(亮度通过 brightnessctl 这类工具控制) IdleTimer(180, action=lazy.spawn("brightnessctl set 30%"), resume=lazy.spawn("brightnessctl set 100%")), # 10 分钟无操作:关闭屏幕 IdleTimer(600, action=lazy.spawn("/usr/bin/xset dpms force off")), # 20 分钟无操作:系统挂起。注意此条强制无视抑制器, # 适合"无论如何都要省电"的笔记本场景 IdleTimer(1200, action=lazy.spawn("systemctl suspend"), respect_inhibitor=False), ]提示:
action/resume不限于lazy.spawn,一切LazyCall(如lazy.group["2"].toscreen()、lazy.widget["textbox"].update(...))都可以作为动作——测试 test_idle_timer 就用lazy.widget["textbox"].update("fired")验证了定时器确实触发。由于IdleTimer的action也接受普通函数与协程,你甚至可以在这里调用自己定义的 Python 函数。
三、Idle inhibitors:定义空闲抑制规则
3.1 配置位置与格式
抑制规则在配置文件的idle_inhibitors段中定义,它是一个IdleInhibitor对象的列表。默认配置中同样为空(resources/default_config.py#L217)。
from libqtile.config import IdleInhibitor, Match from libqtile.lazy import lazy idle_inhibitors = [ IdleInhibitor(match=Match(wm_class="vlc"), when="fullscreen"), ]这个官方示例的含义:当wm_class匹配vlc的窗口处于全屏状态时,激活空闲抑制——全屏看视频时屏幕不会被你的IdleTimer关掉。
3.2 IdleInhibitor 构造参数详解
根据 config.py 中 IdleInhibitor 的定义,IdleInhibitor接受两个参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
match | Match对象 | None(匹配所有窗口) | 定义规则适用于哪些窗口。未设置时规则对全部窗口生效 |
when | 字符串 | "open" | 窗口处于什么状态时抑制器才算激活 |
when支持四个取值,语义从源码 docstring(config.py#L1230-L1236)与 Inhibitor.check() 的实现中可以精确对应:
when取值 | 激活条件 | 源码中的判断 |
|---|---|---|
"focus" | 匹配窗口是当前聚焦窗口 | qtile.current_window == self.window |
"fullscreen" | 匹配窗口处于全屏状态 | window.fullscreen为真 |
"visible" | 匹配窗口在任意屏幕上可见(即使被浮动窗口完全盖住也算可见) | window.is_visible() |
"open" | 匹配窗口已打开(即使被隐藏) | 只要 inhibitor 仍存在于列表中即视为激活 |
注意文档与源码的一处细节:文档示例中when写的是"fullscreen",而源码 docstring 标注的默认值是"open"。因此写配置时务必显式给出你想要的when,不要依赖"默认行为"的直觉。
两个容易踩坑的行为提示(均来自源码):
- 匹配只评估一次:Qtile 在窗口首次创建时评估一次该规则是否命中(
add_config_inhibitors在窗口创建时调用,见 base/window.py#L634-L637);之后when状态的变更由钩子实时跟踪,但match是否命中不会再重新评估。也就是说,先开窗口再改窗口标题不会改变抑制规则是否命中。 - 默认
"open"会"一直生效":一个IdleInhibitor(match=Match(wm_class="vlc"))(不写when)意味着只要 vlc 窗口存在,空闲定时器就永远被抑制——因为"open"状态下窗口只要打开就算激活,而这通常不是你想要的效果。请结合需求明确指定when。
3.3 抑制器的管理生命周期
从源码看,抑制器的完整生命周期是这样的:
- 配置加载:Qtile 启动时,core/manager.py 检测到
config.idle_inhibitors非空,就调用idle_inhibitor_manager.set_hooks(),订阅focus_change与startup两个钩子(idle_inhibit.py#L98-L100)——前者用于在焦点变化时重新评估抑制状态,后者用于在启动时把配置规则应用到所有已存在的窗口。 - 窗口创建:新窗口通过
add_config_inhibitors()逐条比对idle_inhibitors规则,命中者调用add_idle_inhibitor(rule.when)注册进管理器(base/window.py#L634-L647)。 - 状态评估:
IdleInhibitorManager.check()遍历所有 inhibitor,任一激活即把core.inhibited置为True(idle_inhibit.py#L144-L146);而core.inhibited一旦变化会触发idle_inhibitor_change钩子(base/core.py#L168-L172),可用于在你的配置里做自定义响应。 - 运行时控制:除了配置规则,你还可以在运行时动态增减抑制器:
core.set_idle_inhibitor()/core.remove_idle_inhibitor():创建/移除全局抑制器(base/core.py#L174-L182)。- 窗口级
add_idle_inhibitor(inhibitor_type)/remove_idle_inhibitor()命令(base/window.py#L639-L652),可通过qtile-cmd/dqtile-cmd或键绑定调用。 core.get_idle_inhibitors(active_only=True):列出当前激活的抑制器(base/core.py#L184-L191),用于调试。
四、后端实现:X11 与 Wayland 的不同路径
4.1 X11:基于 MIT Screen Saver 扩展
X11 后端的 IdleNotifier 依赖MIT-SCREEN-SAVER 扩展(xcffib.screensaver):
- 启动时检查
has_screen_saver;如果 X server 未提供该扩展,会打印警告并不运行任何定时器(x11/idle_notify.py#L21-L26)。 - 多个 timeout 通过递进间隔实现:
timeout_increments把[300, 900]转换成[300, 600],第一个间隔到达后触发 300 秒的动作并切换到下一个间隔(x11/idle_notify.py#L12-L15)。 - 事件处理:收到
State.On/State.Cycle事件触发handle_timeout,收到State.Off事件触发handle_resume(x11/idle_notify.py#L57-L72)。
4.2 Wayland:原生协议支持
Wayland 后端的 IdleNotifier 通过 CFFI 调用底层 C 实现(qw/server.c):
run()把每个 timeout 注册为qw_server_add_idle_timer;clear_timers()调用qw_server_remove_idle_timer(wayland/idle_notify.py#L9-L17)。- 底层用
wl_list维护idle_timers与idle_inhibitors两个链表(qw/server.h#L279-L282)。 - 同时原生支持
ext_idle_notifier_v1和zwp_idle_inhibit_manager_v1协议:客户端(如swayidle)可以通过协议请求抑制,Qtile 用WaylandInhibitor封装这类"应用级抑制器",并在窗口可见时生效(wayland/idle_inhibit.py#L38-L47)。WaylandInhibitor 还额外处理了 layer surface 与 session lock 场景——会话锁定时只允许来自 session lock 的抑制器生效(wayland/idle_inhibit.py#L31-L49)。
两种抑制器来源(用户配置 vs. Wayland 客户端协议)在 IdleInhibitorManager 中被统一管理:update_user_inhibitors()会先清除非应用级抑制器再重新应用配置规则(idle_inhibit.py#L148-L154)。
五、测试用例:行为如何被验证
仓库的测试对这套机制覆盖得相当细致,是理解语义的"第二份文档":
- 排序与去重:
test_timer_sorting验证无论输入顺序如何,timeouts都按升序、去重输出(test/backend/test_idle_notify.py#L61-L72)。 - 触发与恢复:
test_idle_timer用真实配置驱动,先断言定时器把 textbox 更新为"fired",再手动触发fire_resume()断言更新为"resumed"(test/backend/test_idle_notify.py#L75-L81)。 - 抑制语义:
test_idle_timer_inhibited创建全局抑制器后断言:respect_inhibitor=False的定时器照常触发(切到 group 2),而默认尊重抑制器的定时器保持"unset"不触发(test/backend/test_idle_notify.py#L84-L97)。 - 抑制器规则:test/backend/test_idle_inhibit.py 中同时配置了
when="fullscreen"(任意全屏窗口)、when="focus"、when="visible"、when="open"四种规则,逐一验证状态判定逻辑。
六、实战建议与常见问题
- 先确认后端能力:X11 下请确认 X server 支持 MIT-SCREEN-SAVER 扩展(绝大多数发行版默认支持);Wayland 下则依赖协议实现(Qtile 自带,
swayidle等客户端可直接对接)。 - 权限问题:
action里的命令由 Qtile 进程执行,涉及硬件控制(如调亮度、DPMS、挂起)时请确保对应工具对当前用户可用、无需 root。 - 避免抑制器"常驻":
IdleInhibitor(match=..., when="open")会让匹配窗口一旦打开就永久抑制空闲,务必确认这是你要的效果,或者改用"focus"/"fullscreen"/"visible"。 - 调试技巧:在
qtile-cmd/dqtile-cmd中调用core.get_idle_inhibitors(active_only=True)查看当前哪些抑制器处于激活状态;日志中IdleTimer command error与Error when trying to run idle timer command.(idle_notify.py#L27、idle_notify.py#L36)会给出动作执行失败的原因。 - 配置引用:完整的
IdleTimer/IdleInhibitor类定义见 libqtile/config.py,后端抽象基类见 libqtile/backend/base/idle_notify.py 与 libqtile/backend/base/idle_inhibit.py,默认空配置见 libqtile/resources/default_config.py,官方命令/接口文档位于 docs/manual/commands/。
把idle_timers与idle_inhibitors组合使用,你就能在 Qtile 上得到一套完整、可控、可随时用Match精细调优的电源与空闲管理方案——无论是笔记本合盖前的自动挂起,还是全屏观影时的"永不熄屏",都可以用几行纯 Python 配置优雅实现。
- 桌面应用
- 操作系统
【免费下载链接】qtile
:cookie: A full-featured, hackable tiling window manager written and configured in Python (X11 + Wayland)
相关推荐
bootstrap-datepicker事件系统:事件命名规则与实战指南
bootstrap datepicker事件系统:事件命名规则与实战指南 你是否在使用bootstrap datepicker时遇到过事件监听失效的问题?是否疑
前端UI组件openFrameworks事件系统:ofEvent、监听器与定时器完全指南
openFrameworks事件系统:ofEvent、监听器与定时器完全指南 openFrameworks 是一款社区开发的跨平台 C++ 创意编程工具包,其
图形学音视频ZMK 事件系统完全指南:事件管理器 API、订阅机制与自定义事件开发实战
ZMK 事件系统完全指南:事件管理器 API、订阅机制与自定义事件开发实战 导读 ZMK(Zephyr Mechanical Keyboard firmware
固件嵌入式智能硬件蓝牙
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考