Skip to content

Commit 9db13c7

Browse files
author
hpcdgrie
committed
Added script to configure cover fullscreen on a single display (windows only)
Usage: python winFullScreensConfig.py <display_index> <configFile>
1 parent 59e6668 commit 9db13c7

1 file changed

Lines changed: 235 additions & 0 deletions

File tree

scripts/winFullScreenConfig.py

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import argparse
2+
import ctypes
3+
import re
4+
import xml.etree.ElementTree as ET
5+
from ctypes import wintypes
6+
7+
# usage: winFullScreenConfig.py [display_selector xml_file]
8+
# If no arguments are given, prints all display geometries.
9+
# Otherwise, updates the given config XML file with the geometry of the selected display.
10+
# The display selector can be a numeric display ID or one of: primary, secondary, prefer-secondary.
11+
# The script uses Windows API calls to query display information and modify the XML config accordingly.
12+
13+
DISPLAY_DEVICE_ATTACHED_TO_DESKTOP = 0x00000001
14+
DISPLAY_DEVICE_PRIMARY_DEVICE = 0x00000004
15+
ENUM_CURRENT_SETTINGS = -1
16+
CCHDEVICENAME = 32
17+
CCHFORMNAME = 32
18+
19+
20+
class POINTL(ctypes.Structure):
21+
_fields_ = [
22+
("x", wintypes.LONG),
23+
("y", wintypes.LONG),
24+
]
25+
26+
27+
class DISPLAY_DEVICEW(ctypes.Structure):
28+
_fields_ = [
29+
("cb", wintypes.DWORD),
30+
("DeviceName", wintypes.WCHAR * 32),
31+
("DeviceString", wintypes.WCHAR * 128),
32+
("StateFlags", wintypes.DWORD),
33+
("DeviceID", wintypes.WCHAR * 128),
34+
("DeviceKey", wintypes.WCHAR * 128),
35+
]
36+
37+
38+
class DEVMODEW(ctypes.Structure):
39+
_fields_ = [
40+
("dmDeviceName", wintypes.WCHAR * CCHDEVICENAME),
41+
("dmSpecVersion", wintypes.WORD),
42+
("dmDriverVersion", wintypes.WORD),
43+
("dmSize", wintypes.WORD),
44+
("dmDriverExtra", wintypes.WORD),
45+
("dmFields", wintypes.DWORD),
46+
("dmPosition", POINTL),
47+
("dmDisplayOrientation", wintypes.DWORD),
48+
("dmDisplayFixedOutput", wintypes.DWORD),
49+
("dmColor", wintypes.SHORT),
50+
("dmDuplex", wintypes.SHORT),
51+
("dmYResolution", wintypes.SHORT),
52+
("dmTTOption", wintypes.SHORT),
53+
("dmCollate", wintypes.SHORT),
54+
("dmFormName", wintypes.WCHAR * CCHFORMNAME),
55+
("dmLogPixels", wintypes.WORD),
56+
("dmBitsPerPel", wintypes.DWORD),
57+
("dmPelsWidth", wintypes.DWORD),
58+
("dmPelsHeight", wintypes.DWORD),
59+
("dmDisplayFlags", wintypes.DWORD),
60+
("dmDisplayFrequency", wintypes.DWORD),
61+
("dmICMMethod", wintypes.DWORD),
62+
("dmICMIntent", wintypes.DWORD),
63+
("dmMediaType", wintypes.DWORD),
64+
("dmDitherType", wintypes.DWORD),
65+
("dmReserved1", wintypes.DWORD),
66+
("dmReserved2", wintypes.DWORD),
67+
("dmPanningWidth", wintypes.DWORD),
68+
("dmPanningHeight", wintypes.DWORD),
69+
]
70+
71+
72+
def get_display_id(device_name):
73+
match = re.search(r"DISPLAY(\d+)$", device_name)
74+
if match:
75+
return int(match.group(1))
76+
return None
77+
78+
79+
def get_displays():
80+
displays = []
81+
index = 0
82+
83+
while True:
84+
device = DISPLAY_DEVICEW()
85+
device.cb = ctypes.sizeof(DISPLAY_DEVICEW)
86+
if not ctypes.windll.user32.EnumDisplayDevicesW(None, index, ctypes.byref(device), 0):
87+
break
88+
89+
if not (device.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP):
90+
index += 1
91+
continue
92+
93+
devmode = DEVMODEW()
94+
devmode.dmSize = ctypes.sizeof(DEVMODEW)
95+
if ctypes.windll.user32.EnumDisplaySettingsW(device.DeviceName, ENUM_CURRENT_SETTINGS, ctypes.byref(devmode)):
96+
displays.append({
97+
"display_id": get_display_id(device.DeviceName),
98+
"name": device.DeviceName,
99+
"label": device.DeviceString,
100+
"left": devmode.dmPosition.x,
101+
"top": devmode.dmPosition.y,
102+
"width": devmode.dmPelsWidth,
103+
"height": devmode.dmPelsHeight,
104+
"right": devmode.dmPosition.x + devmode.dmPelsWidth,
105+
"bottom": devmode.dmPosition.y + devmode.dmPelsHeight,
106+
"primary": bool(device.StateFlags & DISPLAY_DEVICE_PRIMARY_DEVICE),
107+
})
108+
109+
index += 1
110+
111+
return displays
112+
113+
114+
def find_display(displays, display_id):
115+
for display in displays:
116+
if display["display_id"] == display_id:
117+
return display
118+
return None
119+
120+
121+
def resolve_display(displays, selector):
122+
normalized = selector.strip().lower()
123+
124+
if normalized == "primary":
125+
for display in displays:
126+
if display["primary"]:
127+
return display
128+
return None
129+
130+
if normalized == "secondary":
131+
secondary_displays = [display for display in displays if not display["primary"]]
132+
if len(secondary_displays) == 1:
133+
return secondary_displays[0]
134+
if not secondary_displays:
135+
return None
136+
raise SystemExit("More than one secondary monitor found. Use the numeric display ID instead.")
137+
138+
if normalized == "prefer-secondary":
139+
secondary_displays = [display for display in displays if not display["primary"]]
140+
if len(secondary_displays) == 1:
141+
return secondary_displays[0]
142+
if len(secondary_displays) > 1:
143+
raise SystemExit("More than one secondary monitor found. Use the numeric display ID instead.")
144+
145+
for display in displays:
146+
if display["primary"]:
147+
return display
148+
return None
149+
150+
try:
151+
display_id = int(selector)
152+
except ValueError as error:
153+
raise SystemExit(
154+
"Display selector must be a numeric display ID or one of: primary, secondary, prefer-secondary."
155+
) from error
156+
157+
return find_display(displays, display_id)
158+
159+
160+
def update_window_config(xml_path, display):
161+
tree = ET.parse(xml_path)
162+
root = tree.getroot()
163+
window = root.find(".//WindowConfig/Window")
164+
if window is None:
165+
raise ValueError("No <Window> element found inside <WindowConfig>.")
166+
167+
window.set("width", str(display["width"]))
168+
window.set("height", str(display["height"]))
169+
window.set("left", str(display["left"]))
170+
window.set("top", str(display["top"]))
171+
172+
tree.write(xml_path, encoding="utf-8", xml_declaration=True)
173+
174+
175+
def print_displays(displays, primary):
176+
selected = [display for display in displays if display["primary"] is primary]
177+
if not selected:
178+
print("No primary monitor found." if primary else "No secondary monitor found.")
179+
return
180+
181+
for index, display in enumerate(selected, 1):
182+
if primary:
183+
label = f"Primary monitor {index}" if len(selected) > 1 else "Primary monitor"
184+
else:
185+
label = f"Secondary monitor {index}" if len(selected) > 1 else "Secondary monitor"
186+
187+
print(f"{label}:")
188+
if display["display_id"] is not None:
189+
print(f" Display : {display['display_id']}")
190+
print(f" Device : {display['name']} ({display['label']})")
191+
print(f" Position : ({display['left']}, {display['top']})")
192+
print(f" Size : {display['width']} x {display['height']} pixels")
193+
print(
194+
f" Rect : left={display['left']}, top={display['top']}, "
195+
f"right={display['right']}, bottom={display['bottom']}"
196+
)
197+
198+
199+
def parse_args():
200+
parser = argparse.ArgumentParser(
201+
description="Print Windows display geometry or apply a display geometry to a COVISE XML config."
202+
)
203+
parser.add_argument(
204+
"display_selector",
205+
nargs="?",
206+
help="Windows display number or one of: primary, secondary, prefer-secondary",
207+
)
208+
parser.add_argument("xml_file", nargs="?", help="Path to the XML config file to modify")
209+
return parser.parse_args()
210+
211+
212+
def main():
213+
args = parse_args()
214+
all_displays = get_displays()
215+
216+
if args.display_selector is None and args.xml_file is None:
217+
print_displays(all_displays, primary=False)
218+
print_displays(all_displays, primary=True)
219+
return
220+
221+
if args.display_selector is None or args.xml_file is None:
222+
raise SystemExit("Provide both display_selector and xml_file, or provide neither.")
223+
224+
display = resolve_display(all_displays, args.display_selector)
225+
if display is None:
226+
raise SystemExit(f"Display {args.display_selector} not found.")
227+
228+
update_window_config(args.xml_file, display)
229+
print(f"Updated {args.xml_file} for display {args.display_selector}.")
230+
print(f" Position : ({display['left']}, {display['top']})")
231+
print(f" Size : {display['width']} x {display['height']} pixels")
232+
233+
234+
if __name__ == "__main__":
235+
main()

0 commit comments

Comments
 (0)