aboutsummaryrefslogtreecommitdiffstats
path: root/xorg-xrandr
diff options
context:
space:
mode:
Diffstat (limited to 'xorg-xrandr')
-rw-r--r--xorg-xrandr/Makefile7
-rw-r--r--xorg-xrandr/setres/Makefile17
-rwxr-xr-xxorg-xrandr/setres/__main__.py225
-rw-r--r--xorg-xrandr/setres/get.py248
-rw-r--r--xorg-xrandr/setres/set.py68
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