|
#!/usr/bin/env python3 |
|
import logging as log |
|
import math |
|
import os |
|
import struct |
|
|
|
import xcffib |
|
import xcffib.xproto |
|
import xcffib.randr |
|
|
|
import pyedid |
|
|
|
from dataclasses import dataclass |
|
|
|
log.basicConfig(level=log.DEBUG) |
|
|
|
conn = xcffib.connect(os.environ["DISPLAY"]) |
|
randr = conn(xcffib.randr.key) |
|
|
|
roots = conn.get_setup().roots |
|
if len(roots) > 1: |
|
log.warning("more than one root window, assuming first is root") |
|
rootid = roots[0].root |
|
|
|
EDID = conn.core.InternAtom(False, len("EDID"), "EDID").reply().atom |
|
|
|
# initialize from xrandr -q --verbse |
|
ssr = randr.GetScreenSizeRange(rootid).reply() |
|
log.debug(f"max width: {ssr.max_width} max height: {ssr.max_height}") |
|
MAX_WIDTH = ssr.max_width |
|
MAX_HEIGHT = ssr.max_height |
|
|
|
sr = randr.GetScreenResources(rootid).reply() |
|
|
|
modes = dict() |
|
for mode in sr.modes: |
|
modes[mode.id] = mode |
|
log.debug(f"modes: {modes}") |
|
|
|
|
|
def get_preferred_mode(output): |
|
return modes[output.modes[output.num_preferred-1]] |
|
|
|
|
|
def dpi_of_mode(output, mode): |
|
diagonal_in = math.sqrt(output.mm_width**2 + output.mm_height**2) / 25.4 |
|
diagonal_px = math.sqrt(mode.width**2 + mode.height**2) |
|
log.debug(f"{diagonal_in} inches {diagonal_px} px") |
|
return int(diagonal_px / diagonal_in) |
|
|
|
|
|
def compute_dpi(output): |
|
log.debug(f"{output.name.to_string()}: {output.num_preferred} {output.modes}") |
|
mode = get_preferred_mode(output) |
|
return dpi_of_mode(output, mode) |
|
|
|
|
|
outputs = [] |
|
for output in sr.outputs: |
|
info = randr.GetOutputInfo(output, xcffib.CurrentTime).reply() |
|
|
|
if info.connection != xcffib.randr.Connection.Connected: |
|
continue |
|
|
|
log.debug(f"{output}: {info.name.to_string()} preferred: {info.num_preferred}, dpi: {compute_dpi(info)}") |
|
edid_raw = randr.GetOutputProperty(output, EDID, 0, 0, 256, False, False).reply().data |
|
edid = pyedid.parse_edid(bytes(edid_raw)) |
|
info.id = output |
|
info.serial = edid.serial |
|
outputs.append(info) |
|
|
|
# if we're just on a laptop, no need to do anything |
|
if len(outputs) == 1: |
|
os._exit(0) |
|
|
|
# more than one output -> disable the laptop |
|
for output in outputs: |
|
# disable the laptop output |
|
if 'eDP-1' == output.name.to_string() or 'eDP1' == output.name.to_string(): |
|
# turn it off if it was on |
|
if output.crtc: |
|
randr.SetCrtcConfig(output.crtc, xcffib.CurrentTime, xcffib.CurrentTime, 0, 0, 0, xcffib.randr.Rotation.Rotate_0, 0, []).reply() |
|
outputs.remove(output) |
|
break |
|
|
|
# monitors should be in the order of their serial |
|
serials_list = ["M2GCR1AM28PL", "1B8W0P3", "M2GCR1AS21NL"] |
|
outputs.sort(key=lambda info: serials_list.index(info.serial)) |
|
|
|
# find highest dpi |
|
max_dpi = max(map(compute_dpi, outputs)) |
|
|
|
total_width_mm = 0 |
|
total_height_mm = 0 |
|
|
|
total_width_px = 0 |
|
total_height_px = 0 |
|
|
|
resolutions = dict() |
|
|
|
def double_to_xfixed(d): |
|
return struct.unpack("i", struct.pack("i", int(d * 65536)))[0] |
|
|
|
@dataclass |
|
class XY: |
|
x: int |
|
y: int |
|
|
|
for output in outputs: |
|
# here, everything is in one big line on the x axis, and nothing is rotated |
|
total_width_mm = total_width_mm + output.mm_width |
|
total_height_mm = max(total_height_mm, output.mm_height) |
|
|
|
this_dpi = compute_dpi(output) |
|
mode = get_preferred_mode(output) |
|
resolution = XY(mode.width, mode.height) |
|
if this_dpi != max_dpi: |
|
# choose the mode closest to the preferred mode scaled by |
|
# max_dpi / this_dpi |
|
resolution = XY(int((max_dpi / this_dpi) * mode.width), int((max_dpi / this_dpi) * mode.height)) |
|
|
|
resolutions[output] = resolution |
|
total_width_px = total_width_px + resolution.x |
|
total_height_px = max(total_height_px, resolution.y) |
|
|
|
for output, r in resolutions.items(): |
|
log.debug(f"output {output.id} getting mode {r.x}x{r.y}") |
|
|
|
try: |
|
conn.core.GrabServer(is_checked=True).check() |
|
|
|
# configure screen size |
|
log.debug(f"setting screen size to {total_width_px}x{total_height_px}px {total_width_mm}x{total_height_mm}mm") |
|
randr.SetScreenSizeChecked(rootid, total_width_px, total_height_px, total_width_mm, total_height_mm).check() |
|
|
|
# set config for outputs |
|
offset_x = 0 |
|
for output in outputs: |
|
resolution = resolutions[output] |
|
mode = get_preferred_mode(output) |
|
my_offset = offset_x |
|
offset_x = offset_x + resolution.x |
|
if not output.crtc: |
|
continue |
|
|
|
# FIXED in render is 16 bits int, 16 bits |
|
width_scale = double_to_xfixed(resolution.x / mode.width) |
|
height_scale = double_to_xfixed(resolution.y / mode.height) |
|
xform = xcffib.render.TRANSFORM.synthetic(width_scale, 0, 0, 0, height_scale, 0, 0, 0, double_to_xfixed(1)) |
|
filter_name = "bilinear" |
|
randr.SetCrtcTransformChecked(output.crtc, xform, len(filter_name), filter_name, 0, []).check() |
|
|
|
config_reply = randr.SetCrtcConfig(output.crtc, xcffib.CurrentTime, xcffib.CurrentTime, my_offset, 0, mode.id, xcffib.randr.Rotation.Rotate_0, 1, [output.id]).reply() |
|
if config_reply.status != xcffib.randr.SetConfig.Success: |
|
raise Exception(f"SetCrtcConfig failed: {config_reply.status}") |
|
finally: |
|
conn.core.UngrabServer() |