1. 核心概念:EDID(扩展显示识别数据)
显示器插入后,显卡通过 I²C 总线读取显示器固件中存储的 EDID 数据块(128 字节或 256 字节)。
Linux 内核(DRM 子系统)会将此二进制数据暴露给用户空间。所有厂商、序列号、支持的分辨率都来源于此。
2. 信息获取的数据源(三层架构)
| 层级 | 路径/命令 | 作用 | 可靠性 |
|---|---|---|---|
| 内核原始数据 | /sys/class/drm/card*-HDMI-*/edid | 直接读取显示器的 EDID 原始二进制字节 | 最高(硬件级) |
| 内核解析属性 | /sys/class/drm/card*-HDMI-*/edid_parsed (部分内核有) | 内核预解析的文本信息 | 中等 |
| 用户态工具 | xrandr --verbose | X/Wayland 服务层解析后的显示信息 | 高(含当前模式) |
| GNOME 配置 | ~/.config/monitors.xml | GNOME 根据上述信息生成的持久化配置 | 用于匹配 |
3. 关键字段提取方法论(EDID 二进制解析)
根据你的脚本,以下是针对 EDID 字节流的精确提取逻辑(遵循 VESA E-EDID 标准 1.3/1.4):
3.1 序列号 (Serial Number) —— 重点
EDID 中存在两种序列号存储方式,脚本采用了优先策略:
-
优先(现代显示器):描述符块 (Descriptor Block) 中的 Tag 0xFF(字符串序列号)。
- 位置:EDID 偏移量 54, 72, 90, 108 处(每块 18 字节)。
- 判定:检查
byte[offset+2] == 0xFF。 - 提取:取
offset+5至offset+18,截断换行符0x0A和空字符0x00,转为 ASCII。 - 优点:通常是纯数字/字母组合,如 “SN123456”,GNOME 直接使用此字符串。
-
回退(老款/廉价显示器):基础 EDID 结构体中的 4 字节整数。
- 位置:Byte 12 - 15(小端序 Little-Endian)。
- 提取:
struct.unpack_from("<I", edid_bytes, 12)[0]。 - 格式化:转为 16 进制字符串(如
0x0000A1B2)。
关键经验:如果显示器序列号在
monitors.xml中显示为0x...,说明它没有字符串序列号,走的是回退逻辑。
3.2 厂商 ID (Vendor / Manufacturer)
- 位置:EDID Byte 8 - 9。
- 编码:非 ASCII 直接存储,而是压缩的 5-bit 字符(基于 ASCII 字母序号)。
- 脚本解码公式:
w = (byte8 << 8) | byte9 # 大端序读取 char1 = chr(((w >> 10) & 0x1F) + 0x40) # 0x40 = '@',因为 PNP ID 从 'A' 开始 char2 = chr(((w >> 5) & 0x1F) + 0x40) char3 = chr((w & 0x1F) + 0x40) - 示例:AOC 的 PNP ID 是
ACI,三星是SAM,LG 是GSM。
3.3 产品型号 / 名称 (Product / Model Name)
脚本采用两阶段搜索,极其健壮:
-
标准方法:在描述符块中查找 Tag 0xFC(Monitor Name)。
- 头部标识:
00 00 00 FC。 - 位置:找到头部后,从
index + 5开始读取最多 13 字节。 - 清洗:截断
0x0A或0x00,解码 ASCII。 - 示例:提取出 “Q27N2”。
- 头部标识:
-
模糊启发式(兜底):针对损坏或非标准 EDID,遍历偏移 54~126 之间的字节,寻找连续 5 个以上可打印 ASCII 字符,并提取为产品名。
3.4 分辨率与刷新率 (Resolution & Refresh Rate)
脚本不依赖 EDID 解析(因为 EDID 包含所有支持的模式,但无法确定当前正在使用的模式),而是调用 xrandr:
- 获取当前活动模式:执行
xrandr --query。 - 正则匹配:查找
HDMI-2 connected下带*(星号)的行,匹配(\d+)x(\d+).*?([\d.]+)Hz。 - 原因:当前分辨率由桌面环境/用户决定,不一定等于 EDID 首选模式。
4. 完整的自动识别工作流(脚本流程拆解)
若需自己实现类似功能,推荐按以下顺序执行:

5. 与 GNOME monitors.xml 的映射关系
GNOME 的 monitors.xml 靠 <monitorspec> 来唯一识别一台物理显示器。脚本生成的新配置完美对应如下结构:
<monitorspec>
<connector>HDMI-2</connector> <!-- 来自扫描的硬件接口 -->
<vendor>AOC</vendor> <!-- 来自 EDID 解码的 PNP ID -->
<product>Q27N2</product> <!-- 来自 EDID 0xFC 描述符 -->
<serial>0x000002e4</serial> <!-- 来自 EDID 序列号提取 -->
</monitorspec>
匹配铁律:GNOME 优先使用
connector+serial组合匹配。如果serial提取错误,会导致配置无法应用(即使 vendor/product 对)。
6. 实战故障排查及手把手命令
如果不想跑脚本,想手动查看这些信息,可以使用以下命令:
6.1 查看 EDID 原始十六进制(肉眼识别序列号)
# 找到你的 HDMI 接口
ls /sys/class/drm/card*-HDMI-*/edid
# 用 hexdump 查看前 128 字节
sudo hexdump -C /sys/class/drm/card0-HDMI-1/edid | head -n 10
- 偏移
0x0C(12) 处为整型序列号。 - 描述符区域(0x36, 0x48, 0x5A, 0x6C)找
FF和FC标签。
6.2 用 edid-decode 一键解析(推荐安装)
sudo apt install edid-decode
sudo edid-decode /sys/class/drm/card0-HDMI-1/edid
输出会清晰显示:
Manufacturer: AOC Model 0x1234 Serial Number 123456
Monitor Name: Q27N2
6.3 查看 xrandr 中的 EDID 解析信息
xrandr --verbose | grep -A 10 "HDMI-2 connected" | grep -E "Manufacturer|Model|EDID"
7. 常见坑点与解决(知识库避坑指南)
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
monitors.xml 替换失败,提示找不到序列号 | 显示器 EDID 无字符串序列号,脚本回退生成 0x0000...,但配置文件中存的是整数格式(如 12345) | 使用 edid-decode 确认实际值,强制修改 XML 中的 <serial> 标签格式为 0x... 或整数字符串 |
| 产品名显示乱码或空格 | EDID 描述符包含了填充空格(0x20)或非 ASCII 码 | 脚本中已做 .strip() 处理;若仍有问题,手动截断尾部无效字符 |
| 刷新率小数位不匹配 | GNOME 要求极高的浮点数精度匹配 | 生成 XML 时保留 15 位小数(f"{rate:.15f}"),xrandr 输出多少就保留多少 |
| 重启后配置未生效 | monitors.xml 被系统或 GDM 锁定缓存 | 执行 sudo systemctl restart gdm 或注销重新登录,而非仅重启 X |
8. 总结:核心实现伪代码
如果你只想复制最核心的 Python 逻辑用于自己的项目,记住这三板斧:
# 1. 读硬件
edid = open(f"/sys/class/drm/card0-HDMI-{id}/edid", "rb").read()
# 2. 解码三大件(自写或调用 edid-decode)
serial = extract_serial_from_descriptor(edid) or unpack_int(edid[12:16])
vendor = decode_pnp_id(edid[8:10])
model = extract_monitor_name(edid)
# 3. 查当前状态
output = subprocess.check_output(["xrandr", "--query"]).decode()
res = parse_current_mode(output, "HDMI-2")
#!/usr/bin/env python3
"""
auto-fix-monitors-xml.py
功能:
1. 读取已连接的 HDMI-2 显示器的 EDID,获取序列号、厂商、产品名、分辨率、刷新率
2. 在 monitors.xml 中找到该序列号对应的 <configuration> 块
3. 将该配置块替换为:HDMI-2 开启为主屏,HDMI-1(NCS) 禁用,DSI-1 禁用
4. 保存修改后的 monitors.xml
用法:
python3 auto-fix-monitors-xml.py # 默认路径 /mnt/f/monitors.xml
python3 auto-fix-monitors-xml.py --path /path/to/monitors.xml
"""
import os
import re
import sys
import glob
import struct
import subprocess
import argparse
import shutil
from xml.etree import ElementTree as ET
from datetime import datetime
'''
# 调试日志 后期屏蔽
class Tee:
"""同时输出到多个文件对象(如控制台 + 日志文件)"""
def __init__(self, *files):
self.files = files
def write(self, obj):
for f in self.files:
f.write(obj)
f.flush()
def flush(self):
for f in self.files:
f.flush()
# 调试日志 后期屏蔽
'''
# ============================================================
# 1. EDID 读取
# ============================================================
def find_hdmi2_edid():
"""查找 HDMI-2 对应的 EDID 文件路径"""
patterns = [
"/sys/class/drm/card*-HDMI-2/edid",
"/sys/class/drm/card*-HDMI-A-2/edid",
]
for pattern in patterns:
paths = sorted(glob.glob(pattern))
if paths:
return paths[0]
return None
def read_edid_bytes(edid_path):
"""读取 EDID 二进制文件"""
with open(edid_path, "rb") as f:
return f.read()
'''
def extract_serial(edid_bytes):
"""从 EDID byte 12-15 提取序列号,返回 '0x000002e4' 格式"""
serial_int = struct.unpack_from("<I", edid_bytes, 12)[0]
return f"0x{serial_int:08x}"
'''
def extract_serial(edid_bytes):
"""
智能提取 EDID 序列号
1. 优先搜索 Tag 0xFF 的描述符块 (标准字符串格式)
2. 若未找到,回退读取 Byte 12-15 (传统整数格式)
"""
if not edid_bytes or len(edid_bytes) < 128:
return "N/A"
# --- 策略 1: 搜索描述符块中的序列号字符串 (Tag 0xFF) ---
# 结构: 00 00 00 FF 00 [Serial String...]
target_header = b'\x00\x00\x00\xFF'
index = edid_bytes.find(target_header)
if index != -1:
# 字符串内容从 index + 5 开始 (跳过 4字节头 + 1字节填充)
start_pos = index + 5
raw_data = edid_bytes[start_pos : start_pos + 13]
# 清洗数据:截取到换行符(0x0A)或空字符(0x00)之前
serial_str = raw_data.split(b'\x0A')[0].split(b'\x00')[0]
try:
result = serial_str.decode('ascii').strip()
if result: # 确保解码后不为空
return result
except UnicodeDecodeError:
pass # 如果解码失败,说明不是有效 ASCII,继续执行策略 2
# --- 策略 2: 回退到固定位置 Byte 12-15 (小端序整数) ---
try:
serial_int = struct.unpack_from("<I", edid_bytes, 12)[0]
# 只有当数值不为 0 时才返回,避免返回无意义的 0x00000000
if serial_int != 0:
return f"0x{serial_int:08x}"
except Exception:
pass
return "N/A"
def extract_vendor_from_edid(edid_bytes):
"""从 EDID byte 8-9 提取厂商 ID(3个字符的 PNP ID)"""
# EDID 厂商 ID 编码在 byte 8-9,使用 5-bit 压缩
# 参考 VESA EDID 规范
w = struct.unpack_from(">H", edid_bytes, 8)[0]
char1 = chr(((w >> 10) & 0x1F) + 0x40)
char2 = chr(((w >> 5) & 0x1F) + 0x40)
char3 = chr((w & 0x1F) + 0x40)
return char1 + char2 + char3
def extract_model_name_robust(edid_bytes):
"""
增强版 EDID 型号提取器
能够处理非标准偏移或错位的描述符
"""
if not edid_bytes or len(edid_bytes) < 128:
return "N/A"
# 策略 1: 搜索标准的 Monitor Name 描述符头 (00 00 00 FC)
# 标准结构: [2字节任意] [00] [FC] [00] [Name...]
target_header = b'\x00\x00\x00\xFC'
index = edid_bytes.find(target_header)
if index != -1:
# 找到头后,字符串通常从 index + 5 开始
start_pos = index + 5
raw_str = edid_bytes[start_pos : start_pos + 13]
# 清洗数据:截取到换行符(0x0A)或空字符(0x00)之前
model_name = raw_str.split(b'\x0A')[0].split(b'\x00')[0]
try:
return model_name.decode('ascii').strip()
except UnicodeDecodeError:
pass
# 策略 2: 针对你的特殊情况 (硬编码搜索或模糊搜索)
# 如果标准方法失败,尝试在常见的描述符区域 (54字节之后) 寻找可打印的 ASCII 序列
# 你的数据中 Q27N2 位于 offset 120 (0x78)
for i in range(54, 126):
# 简单的启发式:如果连续 5 个字节都是可打印 ASCII,且看起来像型号
chunk = edid_bytes[i:i+5]
if all(32 <= b < 127 for b in chunk):
# 进一步验证:检查前后是否是非打印字符(作为边界)
# 或者是已知的厂商前缀
try:
text = chunk.decode('ascii')
# 排除纯数字或纯空格
if text.strip() and not text.isdigit():
# 这里为了匹配你的 Q27N2,我们取更长一点的串
long_chunk = edid_bytes[i:i+10]
clean_text = long_chunk.split(b'\x0A')[0].split(b'\x00')[0].decode('ascii').strip()
if len(clean_text) >= 3:
return clean_text
except:
continue
return "N/A"
# 使用示例
# model = extract_model_name_robust(edid_bytes)
def extract_product(edid_bytes):
"""从 EDID byte 10-11 提取产品代码"""
product_code = struct.unpack_from("<H", edid_bytes, 10)[0]
return f"{product_code}"
def get_xrandr_info():
"""通过 xrandr --verbose 获取 HDMI-2 的厂商名、产品名、分辨率、刷新率"""
info = {
"vendor": None,
"product": None,
"width": None,
"height": None,
"rate": None,
}
try:
output = subprocess.check_output(
["xrandr", "--query", "--verbose"],
text=True, stderr=subprocess.DEVNULL
)
except (subprocess.CalledProcessError, FileNotFoundError):
print("[警告] 无法执行 xrandr,将只使用 EDID 信息")
return info
# 找到 HDMI-2 的段落并解析
lines = output.split("\n")
in_hdmi2 = False
for i, line in enumerate(lines):
if line.startswith("HDMI-2 connected"):
in_hdmi2 = True
# 解析分辨率(第一行包含分辨率信息)
# 例如: HDMI-2 connected 2560x1440+0+0 (normal left inverted right x axis y axis) ...
m = re.search(r'connected\s+(\d+)x(\d+)', line)
if m:
info["width"] = int(m.group(1))
info["height"] = int(m.group(2))
continue
if in_hdmi2:
# 遇到下一个输出端口行则结束
if re.match(r'^(HDMI|DSI|DP|eDP|VGA)-\d', line):
break
# 提取厂商信息(从 EDID 数据或属性中)
# xrandr --verbose 会显示 "Manufacturer: XXX"
m = re.search(r'Manufacturer:\s*(\S+)', line, re.IGNORECASE)
if m:
info["vendor"] = m.group(1).strip()
# 提取产品名
m = re.search(r'Model:\s*(.+)', line, re.IGNORECASE)
if m:
info["product"] = m.group(1).strip()
# 提取 EDID 原始数据行(如果 vendor/product 没找到,从中解析)
if info["vendor"] is None or info["product"] is None:
m = re.search(r'EDID(_DATA)?:\s*"?(.{256})', line)
# 复杂的 EDID 解析跳过,后面用 EDID 文件
# 提取当前活动的刷新率
# 例如: 2560x1440 (0x05e) 59.950Hz *Current
m = re.search(r'(\d+)x(\d+).*?([\d.]+)Hz\s*\*Current', line)
if m:
info["width"] = int(m.group(1))
info["height"] = int(m.group(2))
info["rate"] = float(m.group(3))
return info
def get_current_resolution_from_xrandr():
"""从 xrandr 查询当前活动的分辨率和刷新率"""
try:
# xrandr 标准输出(非 verbose)更容易解析当前分辨率
output = subprocess.check_output(
["xrandr", "--query"],
text=True, stderr=subprocess.DEVNULL
)
for line in output.split("\n"):
if line.startswith("HDMI-2 connected"):
# 找到当前活动分辨率(带 * 号的行)
# HDMI-2 connected 2560x1440+0+0 (normal left inverted right x axis y axis) 477mm x 268mm
# 下一行是分辨率列表,带 * 的是当前使用的
continue
if "HDMI-2" in line and "connected" in line:
m = re.search(r'connected\s+(\d+)x(\d+)', line)
if m:
return int(m.group(1)), int(m.group(2))
# 查找 * 号标记的当前分辨率
lines = output.split("\n")
for i, line in enumerate(lines):
if line.startswith("HDMI-2 connected"):
# 在后续行中找到带 * 的
for j in range(i+1, min(i+10, len(lines))):
m = re.search(r'^\s+(\d+)x(\d+).*?([\d.]+)Hz\s*\*', lines[j])
if m:
return int(m.group(1)), int(m.group(2)), float(m.group(3))
break
except:
pass
return None, None, None
# ============================================================
# 2. monitors.xml 操作
# ============================================================
def find_config_index_by_serial(xml_root, serial, connector="HDMI-2"):
"""
在 monitors.xml 中找到第一个匹配给定序列号和连接器的 <configuration> 块
返回 (索引, 配置元素)
"""
configurations = xml_root.findall("configuration")
for idx, config in enumerate(configurations):
for logicalmonitor in config.findall("logicalmonitor"):
monitor = logicalmonitor.find("monitor")
if monitor is not None:
spec = monitor.find("monitorspec")
if spec is not None:
conn = spec.find("connector")
ser = spec.find("serial")
if (conn is not None and conn.text == connector
and ser is not None and ser.text == serial):
return idx, config
return None, None
def remove_config_at_index(xml_root, idx):
"""移除指定索引的 <configuration> 块"""
configurations = xml_root.findall("configuration")
if 0 <= idx < len(configurations):
xml_root.remove(configurations[idx])
return True
return False
def build_new_config_xml(serial, vendor, product, width, height, rate):
"""根据检测到的信息构建新的配置 XML 字符串"""
# 如果某些字段缺失,使用默认值
if vendor is None:
vendor = "AOC"
if product is None:
product = "Q27V3"
if width is None:
width = 1920
if height is None:
height = 1200
if rate is None:
rate = 59.884601593017578
# 格式化 rate 确保有足够的小数位
rate_str = f"{rate:.15f}"
xml_str = f""" <configuration>
<logicalmonitor>
<x>0</x>
<y>0</y>
<scale>1</scale>
<primary>yes</primary>
<monitor>
<monitorspec>
<connector>HDMI-2</connector>
<vendor>{vendor}</vendor>
<product>{product}</product>
<serial>{serial}</serial>
</monitorspec>
<mode>
<width>{width}</width>
<height>{height}</height>
<rate>{rate_str}</rate>
</mode>
</monitor>
</logicalmonitor>
<disabled>
<monitorspec>
<connector>HDMI-1</connector>
<vendor>NCS</vendor>
<product>NCS HDMI </product>
<serial>0x00000000</serial>
</monitorspec>
<monitorspec>
<connector>DSI-1</connector>
<vendor>unknown</vendor>
<product>unknown</product>
<serial>unknown</serial>
</monitorspec>
</disabled>
</configuration>"""
return xml_str
def replace_config_in_file(xml_path, old_idx, new_config_xml):
"""
在文本级别替换 monitors.xml 中的配置块
这样可以保留原始的 XML 格式和缩进风格
"""
with open(xml_path, "r", encoding="utf-8") as f:
content = f.read()
# 找到所有 <configuration>...</configuration> 块
pattern = re.compile(r' <configuration>.*?</configuration>', re.DOTALL)
matches = list(pattern.finditer(content))
if old_idx >= len(matches):
print(f"[错误] 索引 {old_idx} 超出配置块数量 {len(matches)}")
return False
start, end = matches[old_idx].start(), matches[old_idx].end()
new_content = content[:start] + new_config_xml + content[end:]
# 备份原文件
backup_path = xml_path + ".bak." + datetime.now().strftime("%Y%m%d_%H%M%S")
shutil.copy2(xml_path, backup_path)
print(f"[信息] 原文件已备份到: {backup_path}")
with open(xml_path, "w", encoding="utf-8") as f:
f.write(new_content)
return True
# ============================================================
# 3. 主流程
# ============================================================
def main():
parser = argparse.ArgumentParser(description="自动修正 monitors.xml 中 HDMI-2 的配置")
parser.add_argument(
"--path", "-p",
default=None,
help="monitors.xml 的路径(默认: ~/.config/monitors.xml)"
)
parser.add_argument(
"--dry-run", "-n",
action="store_true",
help="仅预览,不实际修改文件"
)
args = parser.parse_args()
'''
# 调试信息 后期屏蔽
# 将输出同时写入控制台和日志文件
try:
log_file = open("/var/log/hdmi_debug.log", "a", encoding="utf-8")
sys.stdout = Tee(sys.stdout, log_file)
sys.stderr = Tee(sys.stderr, log_file)
except PermissionError:
print("[警告] 无法写入 /var/log/hdmi_debug.log(无权限),仅输出到控制台")
# 调试信息 后期屏蔽
'''
#xml_path = os.path.abspath(args.path)
if args.path is None:
import pwd
# 1. 如果通过 sudo 运行且有 SUDO_USER(非 root),使用该用户的家目录
sudo_user = os.environ.get("SUDO_USER")
if sudo_user and sudo_user != "root":
home = pwd.getpwnam(sudo_user).pw_dir
xml_path = os.path.join(home, ".config/monitors.xml")
else:
# 2. 以 root 直接运行时,扫描 /home/ 下第一个存在的 monitors.xml
import glob
candidates = sorted(glob.glob("/home/*/.config/monitors.xml"))
if candidates:
xml_path = candidates[0]
print(f"[信息] 自动检测到 monitors.xml: {xml_path}")
else:
# 3. 最后 fallback
home = os.path.expanduser("~")
xml_path = os.path.join(home, ".config/monitors.xml")
else:
xml_path = os.path.abspath(args.path)
dry_run = args.dry_run
print("=" * 60)
print(" monitors.xml 自动配置修正工具")
print("=" * 60)
# ---- 第一步:读取 EDID ----
print("\n[步骤1] 读取 HDMI-2 EDID ...")
edid_path = find_hdmi2_edid()
if not edid_path:
print("[错误] 未找到 HDMI-2 的 EDID 文件!")
print(" 请确认 HDMI-2 已连接显示器")
print(" 当前 DRM 设备列表:")
for p in sorted(glob.glob("/sys/class/drm/card*")):
print(f" - {os.path.basename(p)}")
sys.exit(1)
print(f" EDID 文件: {edid_path}")
edid_bytes = read_edid_bytes(edid_path)
serial = extract_serial(edid_bytes)
edid_vendor = extract_vendor_from_edid(edid_bytes)
#edid_product = extract_product_from_edid(edid_bytes)
edid_modelname = extract_model_name_robust(edid_bytes)
# EDID 厂商 ID 是 PNP ID(如 AOC="ACI"),xrandr 显示的是品牌名
# 优先使用 xrandr 的信息
print(f" 序列号: {serial}")
print(f" 厂商(EDID PNP): {edid_vendor}")
#print(f" 制造商(EDID PRODUCT): {edid_product}")
print(f" 型号名称: {edid_modelname}")
# ---- 第二步:从 xrandr 获取详细显示信息 ----
print("\n[步骤2] 从 xrandr 获取显示信息 ...")
xrandr_info = get_xrandr_info()
width, height, rate = get_current_resolution_from_xrandr()
#vendor = xrandr_info.get("vendor")
vendor = extract_vendor_from_edid(edid_bytes)
#product = xrandr_info.get("product")
product = extract_model_name_robust(edid_bytes)
print(f" 厂商: {vendor or '未检测到(将使用默认)'}")
print(f" 产品: {product or '未检测到(将使用默认)'}")
print(f" 分辨率: {width}x{height}" if width else " 分辨率: 未检测到(将使用默认)")
print(f" 刷新率: {rate} Hz" if rate else " 刷新率: 未检测到(将使用默认)")
# ---- 第三步:读取 monitors.xml ----
print(f"\n[步骤3] 读取 monitors.xml: {xml_path} ...")
if not os.path.exists(xml_path):
print(f"[错误] 文件不存在: {xml_path}")
sys.exit(1)
# 用 ElementTree 解析来查找匹配的配置
tree = ET.parse(xml_path)
root = tree.getroot()
idx, config_elem = find_config_index_by_serial(root, serial, "HDMI-2")
# 先生成目标配置,后续用于对比和操作
new_config_xml = build_new_config_xml(serial, vendor, product, width, height, rate)
if idx is None:
print(f"[警告] monitors.xml 中未找到序列号 {serial} 对应的配置")
print(" 这可能是因为该显示器从未在此系统上配置过")
print(" 脚本将尝试添加到文件末尾")
action = "append"
else:
print(f" 找到匹配配置: 第 {idx+1} 个 <configuration>")
# 读取现有配置文本,判断是否与目标配置一致
with open(xml_path, "r", encoding="utf-8") as f:
content = f.read()
config_pattern = re.compile(r' <configuration>.*?</configuration>', re.DOTALL)
config_matches = list(config_pattern.finditer(content))
existing_config_text = content[config_matches[idx].start():config_matches[idx].end()]
if existing_config_text.strip() == new_config_xml.strip():
print(f" 现有配置与目标配置一致,无需修改")
action = "skip"
else:
action = "replace"
# ---- 第四步:生成并替换配置 ----
action_label = {"replace": "替换", "append": "追加", "skip": "跳过"}.get(action, "未知")
print(f"\n[步骤4] {action_label}配置 ...")
print(" 新配置内容:")
for line in new_config_xml.split("\n"):
print(f" {line}")
if dry_run:
print(f"\n[dry-run 模式] 未做任何修改")
print(" 如需实际修改,去掉 --dry-run / -n 参数再运行")
return
if action == "replace":
success = replace_config_in_file(xml_path, idx, new_config_xml)
elif action == "skip":
print(" 配置一致,无需变更")
success = True
else:
# 追加到文件末尾(在 </monitors> 之前)
with open(xml_path, "r", encoding="utf-8") as f:
content = f.read()
# 备份
backup_path = xml_path + ".bak." + datetime.now().strftime("%Y%m%d_%H%M%S")
shutil.copy2(xml_path, backup_path)
print(f"[信息] 原文件已备份到: {backup_path}")
if "</monitors>" in content:
content = content.replace("</monitors>", new_config_xml + "\n</monitors>")
else:
content += "\n" + new_config_xml + "\n</monitors>"
with open(xml_path, "w", encoding="utf-8") as f:
f.write(content)
success = True
if success:
print("\n[✓] 修改成功!")
print(f" 已更新: {xml_path}")
print("")
print(" 提示:需要重新登录 GNOME 或重启显示器配置才能生效")
print(" 也可运行以下命令立即生效:")
print(" sudo systemctl restart gdm (或 lightdm/sddm,视你的 DM 而定)")
print(" 或者直接拔出再插入 HDMI-2 线缆")
else:
print("\n[✗] 修改失败")
sys.exit(1)
if __name__ == "__main__":
main()

1万+

被折叠的 条评论
为什么被折叠?



