diff options
Diffstat (limited to 'xorg-xrandr')
-rw-r--r-- | xorg-xrandr/Makefile | 7 | ||||
-rw-r--r-- | xorg-xrandr/setres/Makefile | 17 | ||||
-rwxr-xr-x | xorg-xrandr/setres/__main__.py | 225 | ||||
-rw-r--r-- | xorg-xrandr/setres/get.py | 248 | ||||
-rw-r--r-- | xorg-xrandr/setres/set.py | 68 |
5 files changed, 565 insertions, 0 deletions
diff --git a/xorg-xrandr/Makefile b/xorg-xrandr/Makefile index b35df78..d135e27 100644 --- a/xorg-xrandr/Makefile +++ b/xorg-xrandr/Makefile @@ -4,16 +4,23 @@ include ../common.mk XINITRC_ORDER = 15 # Testing that xwallpaper is installed because setres calls it +# Testing that python3 is installed because setres is written in it install: $(CHECK_INSTALLED) xwallpaper + $(CHECK_INSTALLED) python3 mkdir -p -- ~/.config/X11/xinit/xinitrc.d test ! -d ~/.config/X11/xinit/xinitrc.d/$(XINITRC_ORDER)-xorg-xrandr ln -sf -- ~/.dotfiles/xorg-xrandr/xinit ~/.config/X11/xinit/xinitrc.d/$(XINITRC_ORDER)-xorg-xrandr + mkdir -p -- ~/.local/bin + make -C setres + ln -sf -- ~/.dotfiles/xorg-xrandr/setres/setres ~/.local/bin uninstall: -unlink -- ~/.config/X11/xinit/xinitrc.d/$(XINITRC_ORDER)-xorg-xrandr -rmdir -- ~/.config/X11/xinit/xinitrc.d -rmdir -- ~/.config/X11/xinit -rmdir -- ~/.config/X11 + -rmdir -- ~/.local/bin/setres + -make -C setres reallyclean .PHONY: install uninstall diff --git a/xorg-xrandr/setres/Makefile b/xorg-xrandr/setres/Makefile new file mode 100644 index 0000000..770d45d --- /dev/null +++ b/xorg-xrandr/setres/Makefile @@ -0,0 +1,17 @@ +all: setres + +setres.zip: __main__.py get.py set.py + zip $@ $^ + +setres: setres.zip + echo '#!/usr/bin/python3' > $@ + cat setres.zip >> $@ + chmod a+x $@ + +clean: + -rm -r *.zip *.pyc __pycache__/ *~ + +reallyclean: clean + -rm setres + +.PHONY: all clean reallyclean diff --git a/xorg-xrandr/setres/__main__.py b/xorg-xrandr/setres/__main__.py new file mode 100755 index 0000000..26f6f65 --- /dev/null +++ b/xorg-xrandr/setres/__main__.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 + +import sys, os, pwd +from subprocess import Popen, PIPE + +from get import * +from set import * + +args = sys.argv[1:] +home = os.environ['HOME'] if 'HOME' in os.environ else pwd.getpwuid(os.getuid()).pw_dir +session_ = os.environ['SESSION_'] if 'SESSION_' in os.environ else '' +hostname = os.uname().nodename.lower() + +t = lambda lopt, sopt : any(arg in args for arg in (lopt, '-' + lopt, '--' + lopt, sopt, '-' + sopt)) + +mirror = t('mirror', 'm') +swap = t('swap', 's') +tv = t('tv', 't') +wide = t('wide', 'w') +crt = t('crt', 'c') +large = t('large', 'l') +single = t('single', '1') +pretend = t('pretend', 'P') + +[screen] = get_setup() +ok = False + +if pretend: + def apply_setup(display): + print(repr(display.to_xrandr())) + return True + + + +### Configurations + + +if hostname == 'zenith': + prime = screen['DisplayPort-2' if not swap else 'DisplayPort-1'] + sec = screen['DisplayPort-1' if not swap else 'DisplayPort-2'] + embed = None + + prime_alt = None + sec_alt = None + + prime.want_mode = '1920x1200' + sec.want_mode = '1920x1200' + + +else: + print('%s: no configurations found for this machine' % sys.argv[0], file = sys.stderr) + sys.exit(1) + +if not prime.connected and prime_alt is not None: + prime, prime_alt = prime_alt, prime +if not sec.connected and sec_alt is not None: + sec, sec_alt = sec_alt, sec + + +if large: + prime.want_mode = '1792x1344' + sec.want_mode = '1792x1344' + +if crt: + prime.want_mode = '800x600' + sec.want_mode = '800x600' + +if wide: + prime.want_mode = '1920x1080' + sec.want_mode = '1920x1080' + +if tv: + sec.want_mode = '1920x1080' + + +if prime is not None: + prime.want_rate = prime.best_rate(prime.want_mode) +if sec is not None: + sec.want_rate = sec.best_rate(sec.want_mode) +if embed is not None: + embed.want_rate = embed.best_rate(embed.want_mode) + + + + +if '+prime' in args: + prime.connected = True +if '+sec' in args: + sec.connected = True +if '+embed' in args: + embed.connected = True + +if '-prime' in args: + prime.connected = False +if '-sec' in args: + sec.connected = False +if '-embed' in args: + embed.connected = False + + + + +### Apply + +if prime.connected and sec.connected and not single: + display = Display() + ok = True + + output = Output(prime.name) + display.outputs.append(output) + output.mode = prime.want_mode + output.rate = prime.want_rate + output.primary = True + output.relpos = None + output.relto = None + + output = Output(sec.name) + display.outputs.append(output) + output.mode = sec.want_mode + output.rate = sec.want_rate + output.primary = False + output.relpos = 'left-of' if not mirror else 'same-as' + output.relto = prime.name + + if embed is not None: + output = Output(embed.name) + display.outputs.append(output) + output.off = True + + +elif prime.connected: + display = Display() + ok = True + + output = Output(prime.name) + display.outputs.append(output) + output.mode = prime.want_mode + output.rate = prime.want_rate + output.primary = True + + output = Output(sec.name) + display.outputs.append(output) + output.off = True + + if embed is not None: + output = Output(embed.name) + display.outputs.append(output) + output.off = True + + +elif sec.connected: + display = Display() + ok = True + + output = Output(sec.name) + display.outputs.append(output) + output.mode = sec.want_mode + output.rate = sec.want_rate + output.primary = True + + output = Output(prime.name) + display.outputs.append(output) + output.off = True + + if embed is not None: + output = Output(embed.name) + display.outputs.append(output) + output.off = True + + +elif embed is None or not embed.connected: + print('%s: don\'t know how to configure' % sys.argv[0], file = sys.stderr) + + +else: + display = Display() + ok = True + + output = Output(embed.name) + display.outputs.append(output) + output.mode = embed.want_mode + output.rate = embed.want_rate + output.primary = True + + output = Output(prime.name) + display.outputs.append(output) + output.off = True + + output = Output(sec.name) + display.outputs.append(output) + output.off = True + +if ok: + if prime_alt is not None: + output = Output(prime_alt.name) + display.outputs.append(output) + output.off = True + + if sec_alt is not None: + output = Output(sec_alt.name) + display.outputs.append(output) + output.off = True + + ok = apply_setup(display) + + + +### Epilogue + +if not ok: + sys.exit(1) +if pretend: + sys.exit(0) + +[screen] = get_setup() +prime = screen[True] +if prime.position[0] > 0: + print(prime.position[0], flush = True) + +for file in ('%s/.config/background.%s' % (home, session_), '%s/.config/background' % home): + if os.path.exists(file): + try: + os.execlp('xwallpaper', 'xwallpaper', '--zoom', file) + except: + pass diff --git a/xorg-xrandr/setres/get.py b/xorg-xrandr/setres/get.py new file mode 100644 index 0000000..47a6428 --- /dev/null +++ b/xorg-xrandr/setres/get.py @@ -0,0 +1,248 @@ +class XScreen: + def __init__(self, line): + self.number = int(line.split(':')[0].split(' ')[1]) + self.minimum = None + self.current = None + self.maximum = None + self.connectors = [] + for part in line.split(':')[1][1:].split(', '): + words = part.split(' ') + if words[0] == 'minimum': + self.minimum = (int(words[1]), int(words[3])) + elif words[0] == 'current': + self.minimum = (int(words[1]), int(words[3])) + elif words[0] == 'maximum': + self.minimum = (int(words[1]), int(words[3])) + + def __getitem__(self, value): + if isinstance(value, str): + for connector in self.connectors: + if connector.name == value: + return connector + elif value is True: + for connector in self.connectors: + if connector.primary: + return connector + else: + return self.connectors[value] + return None + + +class XConnector: + def __init__(self, line): + line = line.split(' ') + self.name = line[0] + self.connected = line[1] == 'connected' + self.primary = False + self.size = None + self.position = None + self.mode = None + self.rr = None + self.physsize = None + self.modes = [] + self.identifier = None + self.timestamp = None + self.subpixel = None + self.gamma = None + self.brightness = None + self.clones = None + self.crtc = None + self.crtcs = None + self.transform = None + self.filter = None # TODO + self.edid = None + # TODO BACKLIGHT + # TODO Backlight + # TODO scaling mode + # TODO Broadcast RGB + # TODO audio + + if not self.connected: + return + i = 2 + + if i == len(line): + return + if line[i] == 'primary': + self.primary = True + i += 1 + + if i == len(line): + return + word = line[i].replace('-', '+-').replace('+--', '+-') + if len(word.replace('+', '')) + 2 == len(word) and len(word.replace('x', '')) + 1 == len(word): + if word.replace('+', '').replace('x', '').isdigit(): + i += 1 + words = word.split('+') + self.position = (int(words[1]), int(words[2])) + words = words[0].split('x') + self.size = (int(words[0]), int(words[1])) + + if i == len(line): + return + if line[i].startswith('(0x') and line[i].endswith(')'): + self.mode = int(line[i][3 : -1], 16) + i += 1 + + while not line[i].startswith('('): + if self.rr is None: + self.rr = line[i] + else: + self.rr += ' ' + line[i] + i += 1 + while not line[i].endswith(')'): + i += 1 + i += 1 + + line = [word[:-2] for word in line[i:] if word.endswith('mm')] + if len(line) == 2: + self.physsize = (int(line[0]), int(line[1])) + + def __getitem__(self, name): + for mode in self.modes: + if mode.name == name: + return mode + return None + + def best_rate(self, mode): + best = None + if isinstance(mode, str): + mode = tuple(int(x) for x in mode.split('x')) + for m in self.modes: + if m.size == mode: + cand = m.vclock + if cand is None: + continue + if best is None or float(cand) > float(best): + best = cand + return best + + +class XMode: + def __init__(self, line): + line = line.split(' ') + [w, h] = line[0].split('x') + self.size = (int(w), int(h)) + self.name = int(line[1][3 : -1], 16) + self.tclock = line[2][:-3] # MHz + self.hsync = None + self.vsync = None + self.current = False + self.preferred = False + for word in line[3]: + if word.endswith('HSync'): + self.hsync = word[0] + elif word.endswith('VSync'): + self.vsync = word[0] + elif word == '*current': + self.current = True + elif word == '+preferred': + self.preferred = True + + def set_h(self, line): + line = line.split(' ') + self.width = int(line[2]) + self.hstart = int(line[4]) + self.hend = int(line[6]) + self.htotal = int(line[8]) + self.hskew = int(line[10]) + self.hclock = line[12][:-3] # KHz + + def set_v(self, line): + line = line.split(' ') + self.height = int(line[2]) + self.vstart = int(line[4]) + self.vend = int(line[6]) + self.vtotal = int(line[8]) + self.vclock = line[10][:-2] # Hz + + +def get_setup(): + from subprocess import Popen, PIPE + + def is_screen_line(line): + if ':' not in line: + return False + line = line.split(':')[0].split(' ') + if len(line) != 2: + return False + return line[0] == 'Screen' and line[1].isdigit() + + def is_connector_line(line): + return ' ' in line and line.split(' ')[1] in ('connected', 'disconnected') + + def is_mode_line(line): + words = line.split(' ') + if len(words) < 2: + return False + if len(words[0]) - 1 != len(words[0].replace('x', '')): + return False + if not words[0].replace('x', '').isdigit(): + return False + if words[0][0] == 'x' or words[0][-1] == 'x': + return False + if not words[1].startswith('(0x'): + return False + return words[1].endswith(')') + + screens = [] + screen = None + connector = None + mode = None + transx = None + on_edid = False + + xrandr_output = Popen('env LANG=C xrandr --verbose'.split(' '), stdout = PIPE) + xrandr_output = xrandr_output.communicate()[0].decode('utf-8', 'strict').split('\n') + + for line in xrandr_output: + if on_edid and line.startswith('\t\t') and ':' not in line: + connector.edid += line.replace(' ', '') + on_edid = None + elif transx is not None: + transx.append(line.strip()) + if len(transx) == 3: + connector.transform = tuple(tuple(float(x) for x in r.split(' ')) for r in transx) + transx = None + elif is_screen_line(line): + screen = XScreen(line) + screens.append(screen) + elif is_connector_line(line): + connector = XConnector(line) + screen.connectors.append(connector) + else: + line = line.replace('\t', ' ').strip() + while ' ' in line: + line = line.replace(' ', ' ') + words = line.split(' ') + if is_mode_line(line): + mode = XMode(line) + connector.modes.append(mode) + elif line.startswith('h: '): + mode.set_h(line) + elif line.startswith('v: '): + mode.set_v(line) + elif line.startswith('Identifier:'): + connector.identifier = int(words[1][2:], 16) + elif line.startswith('Timestamp:'): + connector.timestamp = int(words[1]) + elif line.startswith('Subpixel:'): + connector.subpixel = ' '.join(words[1:]) + elif line.startswith('Gamma:'): + connector.gamma = tuple(float(x) for x in words[1].split(':')) + elif line.startswith('Brightness:'): + connector.brightness = float(words[1]) + elif line.startswith('Clones:'): + connector.clones = words[1:] + elif line.startswith('CRTCs:'): + connector.crtc = int(words[1]) + elif line.startswith('CRTCs:'): + connector.crtcs = tuple(int(x) for x in words[1:]) + elif line.startswith('Transform:'): + transx = [' '.join(words[1:])] + elif line.startswith('EDID:'): + connector.edid = ''.join(words[1:]) + on_edid = None + on_edid = on_edid is None + + return screens diff --git a/xorg-xrandr/setres/set.py b/xorg-xrandr/setres/set.py new file mode 100644 index 0000000..27533bc --- /dev/null +++ b/xorg-xrandr/setres/set.py @@ -0,0 +1,68 @@ +class Display: + def __init__(self, display = None, deepclone = False): + self.outputs = [] + self.fb = None + self.fbmm = None ## TODO automatically + self.dpi = None ## TODO automatically + if display is not None: + self.outputs = [Output(output) if deepclone else output for output in display.outputs] + self.fb = display.fb + self.fbmm = display.fbmm + self.dpi = display.dpi + + def to_xrandr(self): + ret = ['xrandr'] + if self.fb is not None: ret += ['--fb', self.mode] + if self.fbmm is not None: ret += ['--fbmm', self.pos] + if self.dpi is not None: ret += ['--dpi', self.rate] + for output in self.outputs: + ret += output.to_xrandr() + return ret + + +class Output: + def __init__(self, output): + isnew = not isinstance(output, Output) + self.output = output if isnew else output.output + self.mode = None if isnew else output.mode + self.pos = None if isnew else output.pos + self.rate = None if isnew else output.rate + self.reflect = None if isnew else output.reflect + self.rotate = None if isnew else output.rotate + self.off = False if isnew else output.off + self.crtc = None if isnew else output.crtc + self.gamma = None if isnew else output.gamma + self.brightness = None if isnew else output.brightness + self.panning = None if isnew else output.panning + self.transform = None if isnew else output.transform + self.scale = None if isnew else output.scale + self.scale_from = None if isnew else output.scale_from + self.relpos = None if isnew else output.relpos + self.relto = None if isnew else output.relto + self.primary = False if isnew else output.primary + + def to_xrandr(self): + ret = ['--output', self.output] + if self.mode is not None: ret += ['--mode', self.mode] + if self.pos is not None: ret += ['--pos', self.pos] + if self.rate is not None: ret += ['--rate', self.rate] + if self.reflect is not None: ret += ['--reflect', self.reflect] + if self.rotate is not None: ret += ['--rotate', self.rate] + if self.crtc is not None: ret += ['--crtc', self.crtc] + if self.gamma is not None: ret += ['--gamma', self.gamma] + if self.brightness is not None: ret += ['--brightness', self.brightness] + if self.panning is not None: ret += ['--panning', self.panning] + if self.transform is not None: ret += ['--transform', self.transform] + if self.scale is not None: ret += ['--scale', self.scale] + if self.scale_from is not None: ret += ['--scale-from', self.scale_from] + if self.primary: ret += ['--primary'] + if self.relpos is not None and self.relto is not None: + ret += ['--' + self.relpos, self.relto] + return ret if not self.off else ['--output', self.output, '--off'] + + +def apply_setup(display): + import sys + from subprocess import Popen + p = Popen(display.to_xrandr(), stdin = sys.stdin, stdout = sys.stdout, stderr = sys.stderr) + return p.wait() == 0 |