Arch Linux 下 Qtile 桌面管理器

前言

才折腾完 bspwm 不久(参见Bspwm 桌面 ),因为心心念念的 eaf-brower, 最后还是想换桌面。 EAF 是一个框架,允许在用户在 Emacs 中运行其他程序,比如浏览器等。我超级喜欢这个理念,所以老早就想迁移到 eaf. 但是因为浏览器需要自己调教,要花时间,就一直拖着(虽然时间花在折腾其他东西上)。终于把桌面调好后,发现 bspwm 用不了,这可难受坏了。

于是花了两天时间来尝试 Qtile, 一个用 Python 写的桌面,配置用的自然是 Python 语言,虽然可能涉及效率问题,但是在配置方面应当是比 bspwm 来得舒适的。

安装

对于这种轻量级的桌面非常赞,只需要: yay -S qtile, 又因为有 lightdm, 甚至启动项都不用配,直接就能用了。有一些额外包,如果用它提供的 widget 组件时需要。

配置

相对而言, qtile 的文档写得比较简陋,没有 bspwm 那么详细。

配置路径是 ~/.config/config.py, 样板配置在 /usr/share/doc/qtile/default_config.py.

基本概念

在 qtile 中,显示器是 screen, 工作区是 group, 窗口是 window, 布局是 layout.

键盘映射 Key

在配置文件中,变量 keys 记录了所有的键盘映射,它是一个列表,元素是键盘对象。样板配置里面给出了例子:

mod = "mod4"
keys = [
    Key([mod], "h", lazy.layout.left(), desc="Move focus to left"),
    Key([mod], "l", lazy.layout.right(), desc="Move focus to right"),
    Key([mod], "j", lazy.layout.down(), desc="Move focus down"),
    Key([mod], "k", lazy.layout.up(), desc="Move focus up"),
    Key([mod], "space", lazy.layout.next(), desc="Move window focus to other window"),
    Key([mod, "shift"], "h", lazy.layout.shuffle_left(), desc="Move window to the left"),
    Key([mod, "shift"], "l", lazy.layout.shuffle_right(), desc="Move window to the right"),
    Key([mod, "shift"], "j", lazy.layout.shuffle_down(), desc="Move window down"),
    Key([mod, "shift"], "k", lazy.layout.shuffle_up(), desc="Move window up"),
]

首先定义 mod 变量,它是 mod4, 也就是 windows 键。

键盘对象 Key 接收至少三个参数:

  • 第一个参数是一个列表,表示修饰键,比如上面定义的都需要按住 windows 键,而后四个则需要按住 windows + shift
  • 第二个参数是热键,很容易理解
  • 第三个参数是按键映射的函数。 lazy 表示不是立刻执行,而是调度后执行

例子中的第四个参数是描述,可选。

此外, qtile 还提供了 Vim-like 和 Emacs-like 的按键映射。比如我用的 Emacs-like:

from libqtile.config import EzKey

EzKey.modifier_keys = {
   'W': 'mod4',
   'M': 'mod1',
   'S': 'shift',
   'C': 'control',
}

先是映射修饰符,这里是把 Windows 键映射到 W. 在 Emacs 中 Ctrl + Shift + w 记作 C-W, 没有专门的 shift 按键;但是 qtile 有,于是把它映射到 S, 但这样又与 Emacs 中的 meta (也就是 windows ) 键冲突了,就很讨厌。注意, mod1 表示 Alt 键。

随后的映射就是:


# Win + Ctrl + h => Grow window to the left

Key([mod, "control"], "h", lazy.layout.grow_left())

# Emacs-like
EzKey("W-C-h", lazy.layout.grow_left())

键盘启动程序

lazy.spawn("cmd") 来启动程序。比如把 Win + t 映射到启动 kitty 终端模拟器:

EzKey("W-t", lazy.spawn("kitty"))

spawn 里面是命令行的命令,可以加参数。

键盘启动自定义程序

所有的程序都应该是 LazyCall, 所以不能直接把用户自定义的函数映射到键盘上,需要加一个修饰器:

@lazy.function
def my_function(qtile):
    pass

Key([], "k", my_function())

, 函数固定传入一个 qtile 对象。且在函数体中不能再用 lazy. 想执行程序调用 qtile.cmd_spawn. 想执行切换窗口、工作区等操作,可以用 qtile.groups[i].cmd_toscreen() 找到对应的窗口或工作区来切换。

具体有哪些函数可用,官方并没给,我是手动试出来的。在 Debug 那一节我会给出我自己的笨蛋方法。

工作区 Group

定义工作区:

groups = [Group(str(i)) for i in range(1,10)]
for i in groups:
    keys.extend(
        [
            EzKey("W-%s" % i.name, lazy.group[i.name].toscreen()),
            EzKey("W-S-%s" % i.name, lazy.window.togroup(i.name, switch_group=True)),
        ]
    )

groups.append(Group("10"))
keys.extend(
    [
        EzKey("W-0", lazy.group["10"].toscreen()),
        EzKey("W-S-0", lazy.window.togroup("10", switch_group=True)),
    ]
)

实例化 Group 对象就是定义了一个工作区,传入工作区的名字,为了保持一致,我定义了第 10 号工作区,通过 Win + 0 切换。

布局排版 Layout

Qtile 支持多种布局,可以在文档的 build-in layout 那一节看到相关介绍。我采用的是 Bsp, 即 bspwm 的布局,它根据当前窗口的尺寸长边分割,也就是 bspwm 文档中的 longest-side-scheme:

配置如下:

layouts = [
    layout.Bsp(
        border_focus=dracula_purple,
        border_width=4,
        margin=5,
        fair=False,
    ),
]

dracula_purpleDracula 配色的紫色,自行定义的。 fair 设为 False 分割窗口只会在焦距的窗口分割,不管它的尺寸如何。

注意到 layouts 是一个列表,也就是说可以设置多个布局方式,通过 lazy.next_layout() 来切换。

屏幕 Screen

屏幕同样可以设置多个屏幕对应多种不同的展示内容,目前我是单屏,所以实例化一个 Screen 即可。可以在实例化时传入 wallpaper 参数来设置壁纸,也可以用 wallpaper 的 widget. 我这里直接用 Screen 设置,后续可以调用类方法 set_wallpaper 来方便的切换壁纸。

screens = [
    Screen(
        top=bar.Gap(45),
        bottom=bar.Gap(5),
        left=bar.Gap(5),
        right=bar.Gap(5),
        wallpaper="~/Pictures/output/15.png",
        wallpaper_mode='fill'
    )
]

窗口间距

布局设置时,传入了 margin 参数,它表示窗口的外边距。如此一来,分割的两个窗口之间的距离其实是两倍的 margin, 因为每个窗口都有一个。以水平分割的两个窗口为例,我更希望的是窗口到屏幕边缘与窗口之间的距离相等,如:

所以在设置了布局的 margin 之后,窗口到屏幕的距离需要再加一个 margin, 这样这三个距离都是两倍 margin. 故在上一节屏幕的设定中,左下右都加了一个距离为 5 的 Gap ,也就是空隙。为什么上边距是 45,是要给 eww 状态栏留空间。

窗口浮动

如样本配置中的

floating_layout = layout.Floating(
    float_rules=[
        # Run the utility of 'xprop' to see the wm class and name of an X client.
        *layout.Floating.default_float_rules,
        Match(wm_class="confirmreset"),  # gitk
        Match(wm_class="makebranch"),  # gitk
        Match(wm_class="maketag"),  # gitk
        Match(wm_class="ssh-askpass"),  # ssh-askpass
        Match(title="branchdialog"),  # gitk
        Match(title="pinentry"),  # GPG key password entry
    ]
)

添加规则告诉 qtile, 哪些窗口是浮动的。

钩子 Hook

Qtile 用 hook 来加入定制。比如一个简单开机自启动的例子:

@hook.subscribe.startup_once
def auto_start():
    import subprocess
    import os
    script = os.path.join(os.path.dirname(__file__), "autostart.sh")
    subprocess.Popen(script)

在 hook startup_once 也就是启动时这个函数会执行,且只执行一次,restart Qtile 是不会触发这个钩子的。把要自启动的程序写到同目录下的 autostart.sh 脚本中即可。

或者,我想一些让某些窗口浮动,且居于屏幕中间——虽然可以向 floating_layout 中添加规则,但实际上窗口只是浮动了,但是位置和大小不会改变。例如词典 goldendict:

@hook.subscribe.client_new
def goldendict_rule(window):
    ins, cls = window.window.get_wm_class()
    if cls == "GoldenDict":
        window.floating = True
        window.place(660, 290, 600, 500, 0 , '')

它会在新窗口建立 (钩子 client_new ) 时判断,如果新窗口是 goldendict, 那么浮动,大小设置为 600x500, 且位置居中(通过 x 和 y )来调整。

再比如,我想给一个工作区就显示一个应用:

@lazy.function
def dust_mu4e(qtile):
    g = qtile.groups[8]
    g.cmd_toscreen()
    if not g.windows:
        qtile.cmd_spawn("emacs -f mu4e")

keys.extend(
    [
        EzKey("W-r", dust_mu4e()),
    ]
)

,如此按 Win + r, 就会跳转到工作区 9 (索引比名称少 1),如果工作区没窗口,则打开 mu4e.

更多的钩子可以参考官方文档。

命令行工具

命令行可以用 qtile cmd-obj 来执行操作,完整命令 qtile cmd-obj -o obj -f func.

其中: obj 是对象,有 cmd/bar/group/layout/screen/widget/window/core. cmd 是全局对象,group 是窗口。 func 是要执行的函数,根据对象的不同而不同。

给几个例子:

  • 重新加载配置文件: qtile cmd-obj -o cmd -f reload_config
  • 切换到工作区 4: qtile cmd-obj -o group 4 -f toscreen.

具体还是有些函数,自行 help (用得不太多)。

Debug

首先,qtile 会生成一个日志,路径是 ~/.local/share/qtile/qtile.log. 路径可以调,但我忘了在那个地方看到的,现在找不到了。

调用 from libqtile.utils import logger, 然后用 logger.warn 去输出(debug 等级都不到)。用 obj.__dir__() 来看 obj 有哪些方法可以调用。

后记

没太感觉 qtile 用 Python 来写会占用多大资源或者响应多慢,用起来其实还挺 OK, 配置用我最熟悉的 Python 就写一些比较 hack 的方法去和其他比如 eww 模组联动。

Powered by Org Mode.