1#!/usr/bin/env python3

2#

3# This file is part of Checkbox.

4#

5# Copyright 2022 Canonical Ltd.

6#

7# Authors:

8# Pierre Equoy <pierre.equoy@canonical.com>

9#

10# Checkbox is free software: you can redistribute it and/or modify

11# it under the terms of the GNU General Public License version 3,

12# as published by the Free Software Foundation.

13#

14# Checkbox is distributed in the hope that it will be useful,

15# but WITHOUT ANY WARRANTY; without even the implied warranty of

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the

17# GNU General Public License for more details.

18#

19# You should have received a copy of the GNU General Public License

20# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.

21

22 import gi

23 from glob import glob

24 import os

25 import sys

26 from pathlib import Path

27

28 gi.require_versions({"Gtk": "3.0", "Gdk": "3.0"})

29 from gi.repository import Gdk, Gtk # noqa: E402

30

31

32 def get_sysfs_info():

33 """

34 Go through each graphics cards sysfs entries to find max resolution if

35 connected to a monitor.

36 Return a list of ports with information about them.

37 """

38 ports = glob("/sys/class/drm/card*-*")

39 entries = []

40 for p in ports:

41 with open(Path(p) / "modes") as f:

42 # Topmost line in the modes file is the max resolution

43 max_resolution = f.readline().strip()

44 if max_resolution:

45 # e.g. "/sys/class/drm/card0-HDMI-A-1"

46 port = p.split("/")[-1]

47 width, height = max_resolution.split("x")

48 with open(Path(p) / "enabled") as f:

49 enabled = f.readline().strip()

50 with open(Path(p) / "dpms") as f:

51 dpms = f.readline().strip()

52 with open(Path(p) / "status") as f:

53 status = f.readline().strip()

54 port_info = {

55 "port": port,

56 "width": int(width),

57 "height": int(height),

58 "enabled": enabled, # "enabled" or "disabled"

59 "status": status, # "connected" or "disconnected"

60 "dpms": dpms, # "On" or "Off"

61 }

62 entries.append(port_info)

63 return entries

64

65

66 def get_monitors_info():

67 """

68 Get information (model, manufacturer, resolution) from each connected

69 monitors using Gtk.

70 Return a list of monitors with their information.

71 """

72 Gtk.init()

73 display = Gdk.Display.get_default()

74 monitors = []

75 for i in range(display.get_n_monitors()):

76 mon = display.get_monitor(i)

77 monitor = {

78 "model": mon.get_model(),

79 "manufacturer": mon.get_manufacturer(),

80 "width": mon.get_geometry().width,

81 "height": mon.get_geometry().height,

82 "scale_factor": mon.get_scale_factor(),

83 }

84 monitors.append(monitor)

85 return monitors

86

87

88 if __name__ == "__main__":

89 sysfs_entries = get_sysfs_info()

90 mons_entries = get_monitors_info()

91 total_sysfs_res = 0

92 total_mons_res = 0

93 compositor = os.environ.get("XDG_SESSION_TYPE")

94 print("Current compositor: {}".format(compositor))

95 print()

96 print("Maximum resolution found for each connected monitors:")

97 for p in sysfs_entries:

98 port = p["port"]

99 width = p["width"]

100 height = p["height"]

101 enabled = p["enabled"]

102 status = p["status"]

103 dpms = p["dpms"].lower()

104 print(

105 "\t{}: {}x{} ({}, {}, {})".format(

106 port, width, height, dpms, status, enabled

107 )

108 )

109 # If the monitor is disabled (e.g. "Single Display" mode), don't take

110 # its surface into account.

111 if enabled == "enabled":

112 total_sysfs_res += width * height

113 print()

114 print("Current resolution found for each connected monitors:")

115 for m in mons_entries:

116 model = m["model"]

117 manufacturer = m["manufacturer"]

118 scale = m["scale_factor"]

119 # Under X11, the returned width and height are in "application pixels",

120 # not "device pixels", so it has to be multiplied by the scale factor.

121 # However, Wayland always returns the "device pixels" width and height.

122 #

123 # Example: a 3840x2160 screen set to 200% scale will have

124 # width = 1920, height = 1080, scale_factor = 2 on X11

125 # width = 3840, height = 2160, scale_factor = 2 on Wayland

126 if compositor == "x11":

127 width = m["width"] * m["scale_factor"]

128 height = m["height"] * m["scale_factor"]

129 else:

130 width = m["width"]

131 height = m["height"]

132 print(

133 "\t{} ({}): {}x{} @{}%".format(

134 model, manufacturer, width, height, scale * 100

135 )

136 )

137 total_mons_res += width * height

138 print()

139 if total_sysfs_res == total_mons_res:

140 print("The displays are configured at their maximum resolution.")

141 else:

142 sys.exit(

143 (

144 "The displays do not seem to be configured at their maximum "

145 "resolution.\nPlease switch to the maximum resolution before "

146 "continuing."

147 )

148 )