1#!/usr/bin/env python3

2

3 import argparse

4 import os

5 import subprocess

6 import sys

7 import tarfile

8 import time

9

10 from fractions import Fraction

11 from collections import OrderedDict

12

13 parser = argparse.ArgumentParser()

14 parser.add_argument(

15 "--keyword",

16 default="",

17 help=(

18 "A keyword to distinguish the screenshots "

19 "taken in this run of the script"

20 ),

21)

22 parser.add_argument(

23 "--screenshot-dir",

24 default=os.environ["HOME"],

25 help=(

26 "Specify a directory to store screenshots in. "

27 "Default is %(default)s"

28 ),

29)

30 args = parser.parse_args()

31

32

33 device_context = "" # track what device's modes we are looking at

34 modes = [] # keep track of all the devices and modes discovered

35 highest_modes = [] # list of highest-res modes for each aspect ratio

36 current_modes = [] # remember the user's current settings for cleanup later

37 failures = 0 # count the number of failed modesets

38 failure_messages = [] # remember which modes failed

39 success_messages = [] # remember which modes succeeded

40

41# Run xrandr and ask it what devices and modes are supported

42 xrandrinfo = subprocess.Popen(

43 "xrandr -q --verbose", shell=True, stdout=subprocess.PIPE

44)

45 output = xrandrinfo.communicate()[0].decode().split("\n")

46

47

48# The results from xrandr are given in terms of the available display devices.

49# One device can have zero or more associated modes. Unfortunately xrandr

50# indicates this through indentation and is kinda wordy, so we have to keep

51# track of the context we see mode names in as we parse the results.

52

53 for line in output:

54 # I haven't seen any blank lines in xrandr's output in my tests, but meh

55 if line == "":

56 break

57

58 # luckily the various data from xrandr are separated by whitespace...

59 foo = line.split()

60

61 # Check to see if the second word in the line indicates a new context

62 # -- if so, keep track of the context of the device we're seeing

63 if len(foo) >= 2: # throw out any weirdly formatted lines

64 if foo[1] == "disconnected":

65 # we have a new context, but it should be ignored

66 device_context = ""

67 if foo[1] == "connected":

68 # we have a new context that we want to test

69 device_context = foo[0]

70 elif device_context != "": # we've previously seen a 'connected' dev

71 # mode names seem to always be of the format [horiz]x[vert]

72 # (there can be non-mode information inside of a device context!)

73 x_pos = foo[0].find("x")

74 # for a resolution there has to be at least 3 chars:

75 # a digit, an x, and a digit

76 if x_pos > 0 and x_pos < len(foo[0]) - 1:

77 if "DoubleScan" in foo:

78 # xrandr lists DoubleScan resolutions but cannot set them

79 # so for the purposes of this test let's not use them

80 continue

81 if foo[0][x_pos - 1].isdigit() and foo[0][x_pos + 1].isdigit():

82 modes.append((device_context, foo[0]))

83 # we also want to remember what the current mode is, which xrandr

84 # marks with a '*' character, so we can set things back the way

85 # we found them at the end:

86 if "*current" in foo:

87 current_modes.append((device_context, foo[0]))

88# let's create a dict of aspect_ratio:largest_width for each display

89# (width, because it's easier to compare simple ints when looking for the

90# highest value). Ignore Interlaced modes that are indicated by presence of a

91# trailing 'i' character.

92 top_res_per_aspect = OrderedDict()

93 for adapter, mode in modes:

94 try:

95 if mode[-1] == "i":

96 continue

97 width, height = [int(x) for x in mode.split("x")]

98 aspect = Fraction(width, height)

99 if adapter not in top_res_per_aspect:

100 top_res_per_aspect[adapter] = OrderedDict()

101 cur_max = top_res_per_aspect[adapter].get(aspect, 0)

102 top_res_per_aspect[adapter][aspect] = max(cur_max, width)

103 except Exception as exc:

104 print("Error parsing %s: %s" % (mode, exc))

105

106 highest_modes = []

107 for adapter, params in top_res_per_aspect.items():

108 for aspect, width in params.items():

109 # xrandr can list modes that are unsupported, unity-control-center

110 # defines minimum width and height, below which the resolution

111 # is not listed as a choice in display settings panel in UCC

112 # see should_show_resolution function in cc-display-panel.c

113 # from lp:unity-control-center

114 if width < 675 or width / aspect < 530:

115 continue

116 mode = "{}x{}".format(width, width / aspect)

117 highest_modes.append((adapter, mode))

118

119# Now we have a list of the modes we need to test. So let's do just that.

120 screenshot_path = os.path.join(args.screenshot_dir, "xrandr_screens")

121

122 if args.keyword:

123 screenshot_path = screenshot_path + "_" + args.keyword

124 os.makedirs(screenshot_path, exist_ok=True)

125

126 for mode in highest_modes:

127 message = "Set mode " + mode[1] + " for output " + mode[0]

128 print(message, flush=True)

129 cmd = "xrandr --output " + mode[0] + " --mode " + mode[1]

130 retval = subprocess.call(cmd, shell=True)

131 if retval != 0:

132 failures = failures + 1

133 message = "Failed to set mode " + mode[1] + " for output " + mode[0]

134 print(message, file=sys.stderr, flush=True)

135 else:

136 mode_string = mode[0] + "_" + mode[1]

137 filename = os.path.join(screenshot_path, mode_string + ".jpg")

138 cmd = "gnome-screenshot -f " + filename

139 result = subprocess.run(cmd, shell=True, check=False)

140 if result.returncode != 0:

141 print(

142 "Could not capture screenshot -\n"

143 "you may need to install the package 'gnome-screenshot'.",

144 file=sys.stderr,

145 flush=True,

146 )

147 time.sleep(8) # let the hardware recover a bit

148

149# Put things back the way we found them

150

151 for mode in current_modes:

152 cmd = "xrandr --output " + mode[0] + " --mode " + mode[1]

153 subprocess.call(cmd, shell=True)

154

155# Tar up the screenshots for uploading

156 try:

157 with tarfile.open(screenshot_path + ".tgz", "w:gz") as screen_tar:

158 for screen in os.listdir(screenshot_path):

159 screen_tar.add(screenshot_path + "/" + screen, screen)

160 except (IOError, OSError):

161 pass

162

163 if failures != 0:

164 exit(1)

165 else:

166 exit(0)