问题
众所周知,Ubuntu22.04之后,默认的桌面是Wayland,并且Wayland是新一代的显示架构,这是以后的趋势。然而,在Wayland下并不能使用xrandr
命令去设置屏幕的分辨率、旋转关系。
解决方案
在此前提是,诞生了一个python脚本gnome-randr.py
,这个脚本的使用方法和xrandr一样。
这个项目的来源是:Oschowa / gnome-randr · GitLab
也有人用rust实现了一套跟python一样的应用。链接:maxwellainatchi/gnome-randr-rust: `xrandr` for Gnome/wayland, on distros that don't support `wlr-randr` (github.com)
使用演示
# 加执行权限
chmod a+x gnome-randr.py
# 运行命令
./gnome-randr.py
效果如下:
实现原理
gnome-randr.py
最终通过调用 dc_iface.ApplyMonitorsConfig()
函数来实现屏幕旋转。
让我们逐步分析:
- 获取当前显示配置: 脚本通过 D-Bus 连接到 Mutter 显示服务器,并使用
dc_iface.GetCurrentState()
获取当前的显示配置信息,包括屏幕数量、分辨率、旋转方向等。 - 更新配置: 根据用户提供的命令行参数(例如
--output <output_name> --rotate left
),脚本修改获取到的配置信息。 其中config_info.update_output_config(requested_actions)
函数负责根据命令行参数更新配置。rot_to_trans
函数将left
,right
,normal
,inverted
等字符串转换成 Mutter 理解的数字表示。 - 生成新的逻辑显示器配置:
monmap_to_lm
函数根据更新后的配置信息生成新的逻辑显示器配置 (new_lm
)。 这是实际发送给 Mutter 的数据结构,包含了屏幕的位置、旋转角度、缩放比例等信息。 - 应用配置:
dc_iface.ApplyMonitorsConfig(config_info.serial, requested_actions.config_method, new_lm, {})
这个函数调用是关键。它通过 D-Bus 将新的逻辑显示器配置 (new_lm
) 发送给 Mutter,最终实现屏幕旋转。config_info.serial
参数用于确保配置的原子性,requested_actions.config_method
参数决定配置是临时的还是永久的。
所以,虽然脚本中没有直接的“旋转屏幕”命令,但 dc_iface.ApplyMonitorsConfig()
函数是最终实现旋转的核心,它将新的配置应用到 Mutter 显示服务器。
脚本内容
#!/bin/env python3
import sys, os, dbus
from collections import defaultdict
# from stackoverflow.com/questions/5369723
nested_dict = lambda: defaultdict(nested_dict)
def fatal(str):
print(str)
quit(1)
def warn(str):
print('\n! {} !\n'.format(str))
def usage():
print('usage: {} [options]\n'
'\twhere options are:\n'
'\t--current\n'
'\t--dry-run\n'
'\t--persistent\n'
'\t--global-scale <global-scale>\n'
'\t--output <output>\n'
'\t\t--auto\n'
'\t\t--mode <mode>\n'
'\t\t--rate <rate>\n'
'\t\t--scale <scale>\n'
'\t\t--off\n'
'\t\t--right-of <output>\n'
'\t\t--left-of <output>\n'
'\t\t--above <output>\n'
'\t\t--below <output>\n'
'\t\t--same-as <output>\n'
'\t\t--rotate normal,inverted,left,right\n'
'\t\t--primary\n'.format(os.path.basename(sys.argv[0])))
quit()
def get_mode_by_res(res, monitor):
for md in monitor[1]:
res_str = '{}x{}'.format(md[1], md[2])
if res_str == res:
return md
def get_mode_by_id(mode_id, monitor):
for md in monitor[1]:
if md[0] == mode_id:
return md
def mode_has_rate(res, rate, monitor):
for md in monitor[1]:
res_str = '{}x{}'.format(md[1], md[2])
if res_str == res and round(md[3]) == round(rate):
return md
def get_pref_mode(monitor):
for md in monitor[1]:
if 'is-preferred' in md[6]:
return md
def get_current_mode(monitor):
for md in monitor[1]:
if 'is-current' in md[6]:
return md
def has_scale(scale, mode):
for s in mode[5]:
if s == scale:
return s
def mode_props_to_str(props):
str = ''
if 'is-current' in props:
str += '*'
if 'is-preferred' in props:
str += '+'
if 'is-interlaced' in props:
str += 'i'
return str
def modes_to_str_pretty(modes):
mode_strings = dict()
len_max = 1
for md in modes:
res_str = '{:>13}'.format('{}x{}'.format(md[1], md[2]))
rate_str = '{:>11}'.format('{:>8.2f}{:<3}'
.format(md[3], mode_props_to_str(md[6])))
scale_str = scales_to_str(md[4], md[5])
if not res_str in mode_strings:
mode_strings[res_str] = dict()
mode_strings[res_str]['rate-str'] = rate_str
mode_strings[res_str]['scale-str'] = scale_str
else:
mode_strings[res_str]['rate-str'] += rate_str
len_pre = len(res_str) + len(mode_strings[res_str]['rate-str'])
if len_pre > len_max:
len_max = len_pre
mode_strings[res_str]['len-pre'] = len_pre
str = ''
for res_str, v in mode_strings.items():
ind = len_max - v['len-pre'] + 4
str += res_str + v['rate-str'] + ' ' * ind + v['scale-str'] + '\n'
return str
def scales_to_str(pref_scale, scales):
str = '['
for n in range(len(scales)):
str += 'x{0:.1f}'.format(scales[n])
if scales[n] == pref_scale:
str += '+'
if n + 1 >= len(scales):
str += ']&