#!/usr/bin/python3
# DSL - easy Display Setup for Laptops
-# Copyright (C) 2012-2014 Ralf Jung <post@ralfj.de>
+# Copyright (C) 2012-2015 Ralf Jung <post@ralfj.de>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import argparse, sys, os, re, subprocess
-from gui import getFrontend
-frontend = getFrontend("cli") # the fallback, until we got a proper frontend. This is guaranteed to be available.
+from enum import Enum
+import gui, screen
+frontend = gui.getFrontend("cli") # the fallback, until we got a proper frontend. This is guaranteed to be available.
# for auto-config: common names of internal connectors
commonInternalConnectorPrefixes = ['LVDS', 'eDP']
commonInternalConnectorSuffices = ['', '0', '1', '-0', '-1']
+def commonInternalConnectorNames():
+ for prefix in commonInternalConnectorPrefixes:
+ for suffix in commonInternalConnectorSuffices:
+ yield prefix+suffix
-# this is as close as one can get to an enum in Python
-class RelativeScreenPosition:
- LEFT = 0
- RIGHT = 1
- EXTERNAL_ONLY = 2
- MIRROR = 3
-
- __names__ = {
- 'left': LEFT,
- 'right': RIGHT,
- 'external-only': EXTERNAL_ONLY,
- 'mirror': MIRROR
- }
-
-# storing what's necessary for screen setup
-class ScreenSetup:
- def __init__(self, relPosition, intResolution, extResolution, extIsPrimary = False):
- '''relPosition must be one of the RelativeScreenPosition members, the resolutions must be (width, height) pairs'''
- self.relPosition = relPosition
- self.intResolution = intResolution # value doesn't matter if the internal screen is disabled
- self.extResolution = extResolution
- self.extIsPrimary = extIsPrimary or self.relPosition == RelativeScreenPosition.EXTERNAL_ONLY # external is always primary if it is the only one
-
- def getInternalArgs(self):
- if self.relPosition == RelativeScreenPosition.EXTERNAL_ONLY:
- return ["--off"]
- args = ["--mode", res2xrandr(self.intResolution)] # set internal screen to desired resolution
- if not self.extIsPrimary:
- args.append('--primary')
- return args
-
- def getExternalArgs(self, intName):
- args = ["--mode", res2xrandr(self.extResolution)] # set external screen to desired resolution
- if self.extIsPrimary:
- args.append('--primary')
- # set position
- if self.relPosition == RelativeScreenPosition.LEFT:
- args += ['--left-of', intName]
- elif self.relPosition == RelativeScreenPosition.RIGHT:
- args += ['--right-of', intName]
- elif self.relPosition == RelativeScreenPosition.MIRROR:
- args += ['--same-as', intName]
- else:
- assert self.relPosition == RelativeScreenPosition.EXTERNAL_ONLY
- return args
# Load a section-less config file: maps parameter names to space-separated lists of strings (with shell quotation)
def loadConfigFile(filename):
# add some convencience get functions
return result
-# iterator yielding common names of internal connectors
-def commonInternalConnectorNames():
- for prefix in commonInternalConnectorPrefixes:
- for suffix in commonInternalConnectorSuffices:
- yield prefix+suffix
-
-# helper function: execute a process, return output as iterator, throw exception if there was an error
-# you *must* iterate to the end if you use this!
-def processOutputGen(*args):
- with subprocess.Popen(args, stdout=subprocess.PIPE) as p:
- for line in p.stdout:
- yield line.decode("utf-8")
- if p.returncode != 0:
- raise Exception("Error executing "+str(args))
-def processOutputIt(*args):
- return list(processOutputGen(*args)) # list() iterates over the generator
-
-# Run xrandr and return a dict of output names mapped to lists of available resolutions, each being a (width, height) pair.
-# An empty list indicates that the connector is disabled.
-def getXrandrInformation():
- connectors = {} # map of connector names to a list of resolutions
- connector = None # current connector
- for line in processOutputGen("xrandr", "-q"):
- # screen?
- m = re.search(r'^Screen [0-9]+: ', line)
- if m is not None: # ignore this line
- connector = None
- continue
- # new connector?
- m = re.search(r'^([\w\-]+) (dis)?connected ', line)
- if m is not None:
- connector = m.groups()[0]
- assert connector not in connectors
- connectors[connector] = []
- continue
- # new resolution?
- m = re.search(r'^ ([\d]+)x([\d]+) +', line)
- if m is not None:
- assert connector is not None
- connectors[connector].append((int(m.groups()[0]), int(m.groups()[1])))
- continue
- # unknown line
- # not fatal as my xrandr shows strange stuff when a display is enabled, but not connected
- #raise Exception("Unknown line in xrandr output:\n"+line)
- print("Warning: Unknown xrandr line %s" % line)
- return connectors
-# convert a (width, height) pair into a string accepted by xrandr as argument for --mode
-def res2xrandr(res):
- (w, h) = res
- return str(w)+'x'+str(h)
-
-# convert a (width, height) pair into a string to be displayed to the user
-def res2user(res):
- (w, h) = res
- # get ratio
- ratio = int(round(16.0*h/w))
- if ratio == 12: # 16:12 = 4:3
- strRatio = '4:3'
- elif ratio == 13: # 16:12.8 = 5:4
- strRatio = '5:4'
- else: # let's just hope this will never be 14 or more...
- strRatio = '16:%d' % ratio
- return '%dx%d (%s)' %(w, h, strRatio)
-
-# return the first available connector from those listed in tryConnectors, skipping disabled connectors
-def findAvailableConnector(tryConnectors, allConnectors):
- for connector in tryConnectors:
- if connector in allConnectors and allConnectors[connector]: # if the connector exists and is active (i.e. there is a resolution)
- return connector
- return None
+# Make sure the backlight is turned on
+def turnOnBacklight():
+ try:
+ backlight = float(subprocess.check_output(["xbacklight", "-get"]).strip())
+ if backlight == 0: # it's completely turned off, we better enable it
+ subprocess.check_call(["xbacklight", "-set", "100"])
+ except FileNotFoundError:
+ print("xbacklight has not been found, unable to turn your laptop backlight on.")
+ except subprocess.CalledProcessError:
+ print("xbacklight returned an error while attempting to turn your laptop backlight on.")
-# Return a (internalConnector, externalConnectors) pair: The name of the internal connector, and a list of external connectors.
-# Use the config file at ~/.dsl.conf and fall back to auto-detection
-def classifyConnectors(allConnectors):
- config = loadConfigFile(os.getenv('HOME') + '/.dsl.conf')
- # find internal connector
- if 'internalConnector' in config:
- if len(config['internalConnector']) != 1:
- raise Exception("You must specify exactly one internal connector.")
- internalConnector = config['internalConnector'][0]
- if not internalConnector in allConnectors:
- raise Exception("Connector %s does not exist, there is an error in your config file." % internalConnector)
- else:
- # auto-config
- internalConnector = findAvailableConnector(commonInternalConnectorNames(), allConnectors)
- if internalConnector is None:
- raise Exception("Could not automatically find internal connector, please use ~/.dsl.conf to specify it manually.")
- # all the rest is external then, obviously - unless the user wants to do that manually
- if 'externalConnectors' in config:
- externalConnectors = config['externalConnectors']
- for connector in externalConnectors:
- if not connector in allConnectors:
- raise Exception("Connector %s does not exist, there is an error in your config file." % connector)
- if connector == internalConnector:
- raise Exception("%s is both internal and external, that doesn't make sense." % connector)
- else:
- externalConnectors = list(allConnectors.keys())
- externalConnectors.remove(internalConnector)
- if not externalConnectors:
- raise Exception("No external connector found - either your config is wrong, or your machine has only one connector.")
- # done!
- return (internalConnector, externalConnectors)
# if we run top-level
if __name__ == "__main__":
dest="frontend",
help="The frontend to be used for user interaction")
parser.add_argument("-r", "--relative-position",
- dest="rel_position", choices=RelativeScreenPosition.__names__.keys(),
+ dest="rel_position", choices=list(map(str.lower, screen.RelativeScreenPosition.__members__.keys())),
help="Position of external screen relative to internal one")
parser.add_argument("-i", "--internal-only",
dest="internal_only", action='store_true',
help="Enable internal screen, disable all the others (as if no external screen was connected")
cmdArgs = parser.parse_args()
- # load frontend
- frontend = getFrontend(cmdArgs.frontend)
+ # load frontend early (for error mssages)
+ frontend = gui.getFrontend(cmdArgs.frontend)
try:
- # load connectors and classify them
- connectors = getXrandrInformation()
- (internalConnector, externalConnectors) = classifyConnectors(connectors)
+ # see what situation we are in
+ situation = screen.ScreenSituation(commonInternalConnectorNames())
- # default: screen off
- connectorArgs = {} # maps connector names to xrand arguments
- for c in externalConnectors+[internalConnector]:
- connectorArgs[c] = ["--off"]
-
- # check whether we got an external screen or not
- usedExternalConnector = findAvailableConnector(externalConnectors, connectors) # *the* external connector which is actually used
- hasExternal = not cmdArgs.internal_only and usedExternalConnector is not None
- if hasExternal:
- # compute the list of resolutons available on both
- commonRes = [res for res in connectors[usedExternalConnector] if res in connectors[internalConnector]]
- # there's an external screen connected, we need to get a setup
- if cmdArgs.rel_position is not None:
- # use command-line arguments
- relPosition = RelativeScreenPosition.__names__[cmdArgs.rel_position]
- if relPosition == RelativeScreenPosition.MIRROR:
- setup = ScreenSetup(relPosition, commonRes[0], commonRes[0]) # use default resolutions
- else:
- setup = ScreenSetup(relPosition, connectors[internalConnector][0], connectors[usedExternalConnector][0]) # use default resolutions
- else:
- # use GUI
- setup = frontend.setup(connectors[internalConnector], connectors[usedExternalConnector], commonRes)
+ # construct the ScreenSetup
+ setup = None
+ if situation.externalResolutions() is not None:
+ setup = frontend.setup(situation)
if setup is None: sys.exit(1) # the user canceled
- # apply it
- connectorArgs[internalConnector] = setup.getInternalArgs()
- connectorArgs[usedExternalConnector] = setup.getExternalArgs(internalConnector)
else:
# use first resolution of internal connector
- connectorArgs[internalConnector] = ["--mode", res2xrandr(connectors[internalConnector][0]), "--primary"]
+ setup = ScreenSetup(intResolution = situation.internalResolutions()[0], extResolution = None)
- # and do it
- call = ["xrandr"]
- for name in connectorArgs:
- call += ["--output", name] + connectorArgs[name]
- print("Call that will be made:",call)
- subprocess.check_call(call)
+ # call xrandr
+ xrandrCall = situation.forXrandr(setup)
+ print("Call that will be made:",xrandrCall)
+ subprocess.check_call(xrandrCall)
# make sure the internal screen is really, *really* turned on if there is no external screen
- if not hasExternal:
- try:
- backlight = float(subprocess.check_output(["xbacklight", "-get"]).strip())
- if backlight == 0: # it's completely turned off, we better enable it
- subprocess.check_call(["xbacklight", "-set", "100"])
- except FileNotFoundError:
- print("xbacklight has not been found, unable to turn your laptop backlight on.")
- except subprocess.CalledProcessError:
- print("xbacklight returned an error while attempting to turn your laptop backlight on.")
+ if setup.extResolution is None:
+ turnOnBacklight()
except Exception as e:
frontend.error(str(e))
raise
# DSL - easy Display Setup for Laptops
-# Copyright (C) 2012 Ralf Jung <post@ralfj.de>
+# Copyright (C) 2012-2015 Ralf Jung <post@ralfj.de>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
from PyQt4 import QtGui
QtGui.QMessageBox.critical(None, 'Fatal error', message)
- def setup(self, internalResolutions, externalResolutions, commonRes):
+ def setup(self, situation):
from qt_dialogue import PositionSelection
- return PositionSelection(internalResolutions, externalResolutions, commonRes).run()
+ return PositionSelection(situation).run()
@staticmethod
def isAvailable():
'''Displays a fatal error to the user'''
subprocess.check_call(["zenity", "--error", "--text="+message])
- def setup(self, internalResolutions, externalResolutions, commonRes):
+ def setup(self, situation):
from zenity_dialogue import run
- return run(internalResolutions, externalResolutions)
+ return run(situation.internalResolutions(), situation.externalResolutions())
@staticmethod
def isAvailable():
# DSL - easy Display Setup for Laptops
-# Copyright (C) 2012 Ralf Jung <post@ralfj.de>
+# Copyright (C) 2012-2015 Ralf Jung <post@ralfj.de>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-from dsl import RelativeScreenPosition, ScreenSetup, res2user
-from PyQt4 import QtCore, QtGui
+import os
+from screen import RelativeScreenPosition, ScreenSetup
+from PyQt4 import QtCore, QtGui, uic
+
+relPosNames = {
+ RelativeScreenPosition.LEFT: "left of",
+ RelativeScreenPosition.RIGHT: "right of",
+ RelativeScreenPosition.ABOVE: "above",
+ RelativeScreenPosition.BELOW: "below",
+ RelativeScreenPosition.MIRROR: "same as",
+}
-def makeLayout(layout, members):
- for m in members:
- if isinstance(m, QtGui.QLayout):
- layout.addLayout(m)
- else:
- layout.addWidget(m)
- return layout
class PositionSelection(QtGui.QDialog):
- def __init__(self, internalResolutions, externalResolutions, commonResolutions):
+ def __init__(self, situation):
# set up main window
super(PositionSelection, self).__init__()
- self.setWindowTitle('DSL - easy Display Setup for Laptops')
-
- ## position selection
- posBox = QtGui.QGroupBox('Position of external screen', self)
- self.posLeft = QtGui.QRadioButton('Left of internal screen', posBox)
- self.posRight = QtGui.QRadioButton('Right of internal screen', posBox)
- self.posRight.setChecked(True)
- self.posRight.setFocus()
- self.extOnly = QtGui.QRadioButton('Use external screen exclusively', posBox)
- self.mirror = QtGui.QRadioButton('Mirror internal screen', posBox)
- positions = [self.posLeft, self.posRight, self.extOnly, self.mirror]
- posBox.setLayout(makeLayout(QtGui.QVBoxLayout(), positions))
- for pos in positions:
- pos.toggled.connect(self.updateForm)
-
- ## primary screen
- self.primBox = QtGui.QGroupBox('Which should be the primary screen?', self)
- self.primExt = QtGui.QRadioButton('The external screen', self.primBox)
- self.primInt = QtGui.QRadioButton('The internal screen', self.primBox)
- self.primInt.setChecked(True)
- self.primBox.setLayout(makeLayout(QtGui.QVBoxLayout(), [self.primExt, self.primInt]))
+ self._situation = situation
+ uifile = os.path.join(os.path.dirname(__file__), 'qt_dialogue.ui')
+ uic.loadUi(uifile, self)
- ## resolution selection
- resBox = QtGui.QGroupBox('Screen resolutions', self)
- # external screen
- self.extResLabel = QtGui.QLabel('Resolution of external screen:', resBox)
- self.extResolutions = externalResolutions
- self.extResolutionsBox = QtGui.QComboBox(resBox)
- for res in externalResolutions:
- self.extResolutionsBox.addItem(res2user(res))
- self.extResolutionsBox.setCurrentIndex(0) # select first resolution
- self.extRow = makeLayout(QtGui.QHBoxLayout(), [self.extResLabel, self.extResolutionsBox])
- # internal screen
- self.intResLabel = QtGui.QLabel('Resolution of internal screen:', resBox)
- self.intResolutions = internalResolutions
- self.intResolutionsBox = QtGui.QComboBox(resBox)
- for res in internalResolutions:
- self.intResolutionsBox.addItem(res2user(res))
- self.intResolutionsBox.setCurrentIndex(0) # select first resolution
- self.intRow = makeLayout(QtGui.QHBoxLayout(), [self.intResLabel, self.intResolutionsBox])
- # both screens
- self.mirrorResLabel = QtGui.QLabel('Resolution of both screens:', resBox)
- self.mirrorResolutions = commonResolutions
- self.mirrorResolutionsBox = QtGui.QComboBox(resBox)
- for res in commonResolutions:
- self.mirrorResolutionsBox.addItem(res2user(res))
- self.mirrorResolutionsBox.setCurrentIndex(0) # select first resolution
- self.mirrorRow = makeLayout(QtGui.QHBoxLayout(), [self.mirrorResLabel, self.mirrorResolutionsBox])
- # show them all
- resBox.setLayout(makeLayout(QtGui.QVBoxLayout(), [self.extRow, self.intRow, self.mirrorRow]))
+ # fill relative position box
+ for pos in RelativeScreenPosition:
+ self.relPos.addItem(relPosNames[pos], pos)
- # last row: buttons
- buttons = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel, QtCore.Qt.Horizontal, self)
- buttons.accepted.connect(self.accept)
- buttons.rejected.connect(self.reject)
-
- # add them all to the window
- self.setLayout(makeLayout(QtGui.QVBoxLayout(), [posBox, self.primBox, resBox, buttons]))
-
- # make sure we are consistent
- self.updateForm()
+ # keep resolutions in sync when in mirror mode
+ def syncIfMirror(source, target):
+ def _slot(idx):
+ if self.isMirror:
+ target.setCurrentIndex(idx)
+ source.currentIndexChanged.connect(_slot)
+ syncIfMirror(self.intRes, self.extRes)
+ syncIfMirror(self.extRes, self.intRes)
+
+ # connect the update function, and make sure we are in a correct state
+ self.intEnabled.toggled.connect(self.updateEnabledControls)
+ self.extEnabled.toggled.connect(self.updateEnabledControls)
+ self.relPos.currentIndexChanged.connect(self.updateEnabledControls)
+ self.updateEnabledControls()
+
+ def getRelativeScreenPosition(self):
+ idx = self.relPos.currentIndex()
+ return self.relPos.itemData(idx)
+
+ def fillResolutionBox(self, box, resolutions):
+ # if the count did not change, update in-place (this avoids flicker)
+ if box.count() == len(resolutions):
+ for idx, res in enumerate(resolutions):
+ box.setItemText(idx, str(res))
+ box.setItemData(idx, res)
+ else:
+ # first clear it
+ while box.count() > 0:
+ box.removeItem(0)
+ # then fill it
+ for res in resolutions:
+ box.addItem(str(res), res)
- def updateForm(self):
- self.primBox.setEnabled(self.posLeft.isChecked() or self.posRight.isChecked())
- self.extResolutionsBox.setEnabled(not self.mirror.isChecked())
- self.extResLabel.setEnabled(not self.mirror.isChecked())
- self.intResolutionsBox.setEnabled(self.posLeft.isChecked() or self.posRight.isChecked())
- self.intResLabel.setEnabled(self.posLeft.isChecked() or self.posRight.isChecked())
- self.mirrorResolutionsBox.setEnabled(self.mirror.isChecked())
- self.mirrorResLabel.setEnabled(self.mirror.isChecked())
+ def updateEnabledControls(self):
+ intEnabled = self.intEnabled.isChecked()
+ extEnabled = self.extEnabled.isChecked()
+ bothEnabled = intEnabled and extEnabled
+ self.isMirror = bothEnabled and self.getRelativeScreenPosition() == RelativeScreenPosition.MIRROR # only if both are enabled, we can really mirror
+ # configure screen controls
+ self.intRes.setEnabled(intEnabled)
+ self.intPrimary.setEnabled(intEnabled and not self.isMirror)
+ self.extRes.setEnabled(extEnabled)
+ self.extPrimary.setEnabled(extEnabled and not self.isMirror)
+ if not intEnabled and extEnabled:
+ self.extPrimary.setChecked(True)
+ elif not extEnabled and intEnabled:
+ self.intPrimary.setChecked(True)
+ # which resolutions do we offer?
+ if self.isMirror:
+ commonRes = self._situation.commonResolutions()
+ self.fillResolutionBox(self.intRes, commonRes)
+ self.fillResolutionBox(self.extRes, commonRes)
+ self.intRes.setCurrentIndex(self.extRes.currentIndex())
+ else:
+ self.fillResolutionBox(self.intRes, self._situation.internalResolutions())
+ self.fillResolutionBox(self.extRes, self._situation.externalResolutions())
+ # configure position control
+ self.posGroup.setEnabled(bothEnabled)
+ self.posLabel1.setEnabled(bothEnabled)
+ self.posLabel2.setEnabled(bothEnabled)
+ self.relPos.setEnabled(bothEnabled)
+ # avoid having no screen
+ self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setEnabled(intEnabled or extEnabled)
def run(self):
self.exec_()
if not self.result(): return None
- if self.mirror.isChecked():
- return ScreenSetup(RelativeScreenPosition.MIRROR,
- self.mirrorResolutions[self.mirrorResolutionsBox.currentIndex()],
- self.mirrorResolutions[self.mirrorResolutionsBox.currentIndex()],
- extIsPrimary = True)
- else:
- return ScreenSetup(self.getRelativeScreenPosition(),
- self.intResolutions[self.intResolutionsBox.currentIndex()],
- self.extResolutions[self.extResolutionsBox.currentIndex()],
- self.primExt.isChecked())
-
- def getRelativeScreenPosition(self):
- if self.posLeft.isChecked():
- return RelativeScreenPosition.LEFT
- elif self.posRight.isChecked():
- return RelativeScreenPosition.RIGHT
- elif self.extOnly.isChecked():
- return RelativeScreenPosition.EXTERNAL_ONLY
- else:
- raise Exception("Nothing is checked?")
+ intRes = self.intRes.itemData(self.intRes.currentIndex()) if self.intEnabled.isChecked() else None
+ extRes = self.extRes.itemData(self.extRes.currentIndex()) if self.extEnabled.isChecked() else None
+ return ScreenSetup(intRes, extRes, self.getRelativeScreenPosition(), self.extPrimary.isChecked())
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Dialog</class>
+ <widget class="QDialog" name="Dialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>332</width>
+ <height>217</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>DSL - easy Display Setup for Laptops</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QGroupBox" name="groupBox">
+ <property name="title">
+ <string>Screen Configuratioon</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Enable:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QCheckBox" name="intEnabled">
+ <property name="text">
+ <string>Internal</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="2">
+ <widget class="QCheckBox" name="extEnabled">
+ <property name="text">
+ <string>External</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label_2">
+ <property name="text">
+ <string>Resolution:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QComboBox" name="intRes"/>
+ </item>
+ <item row="1" column="2">
+ <widget class="QComboBox" name="extRes"/>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_3">
+ <property name="text">
+ <string>Primary:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QRadioButton" name="intPrimary">
+ <property name="text">
+ <string>Internal</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="2">
+ <widget class="QRadioButton" name="extPrimary">
+ <property name="text">
+ <string>External</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="posGroup">
+ <property name="title">
+ <string>Screen Position</string>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QLabel" name="posLabel1">
+ <property name="text">
+ <string>External screen is</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="relPos"/>
+ </item>
+ <item>
+ <widget class="QLabel" name="posLabel2">
+ <property name="text">
+ <string>internal screen.</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>1</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>Dialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>Dialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
--- /dev/null
+#!/usr/bin/python3
+# DSL - easy Display Setup for Laptops
+# Copyright (C) 2012-2015 Ralf Jung <post@ralfj.de>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+import re, subprocess
+from enum import Enum
+
+## utility functions
+
+# execute a process, return output as iterator, throw exception if there was an error
+# you *must* iterate to the end if you use this!
+def processOutputGen(*args):
+ with subprocess.Popen(args, stdout=subprocess.PIPE) as p:
+ for line in p.stdout:
+ yield line.decode("utf-8")
+ if p.returncode != 0:
+ raise Exception("Error executing "+str(args))
+def processOutputIt(*args):
+ return list(processOutputGen(*args)) # list() iterates over the generator
+
+## the classes
+
+class RelativeScreenPosition(Enum):
+ '''Represents the relative position of the external screen to the internal one'''
+ LEFT = 0
+ RIGHT = 1
+ ABOVE = 2
+ BELOW = 3
+ MIRROR = 4
+
+
+class Resolution:
+ '''Represents a resolution of a screen'''
+ def __init__(self, width, height):
+ self.width = width
+ self.height = height
+
+ def __eq__(self, other):
+ if not isinstance(other, Resolution):
+ return False
+ return self.width == other.width and self.height == other.height
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def __str__(self):
+ # get ratio
+ ratio = int(round(16.0*self.height/self.width))
+ if ratio == 12: # 16:12 = 4:3
+ strRatio = '4:3'
+ elif ratio == 13: # 16:12.8 = 5:4
+ strRatio = '5:4'
+ else: # let's just hope this will never be 14 or more...
+ strRatio = '16:%d' % ratio
+ return '%dx%d (%s)' %(self.width, self.height, strRatio)
+
+ def __repr__(self):
+ return 'screen.Resolution('+self.forXrandr()+')'
+
+ def forXrandr(self):
+ return str(self.width)+'x'+str(self.height)
+
+
+class ScreenSetup:
+ '''Represents a screen configuration (relative to some notion of an "internal" and an "external" screen): Which screens are enabled with which resolution, how
+ are they positioned, which is the primary screen.'''
+ def __init__(self, intResolution, extResolution, relPosition = None, extIsPrimary = True):
+ '''The resolutions can be None to disable the screen, instances of Resolution. The last two arguments only matter if both screens are enabled.'''
+ assert intResolution is None or isinstance(intResolution, Resolution)
+ assert extResolution is None or isinstance(extResolution, Resolution)
+
+ self.intResolution = intResolution
+ self.extResolution = extResolution
+ self.relPosition = relPosition
+ self.extIsPrimary = extIsPrimary or self.intResolution is None # external is always primary if it is the only one
+
+ def getInternalArgs(self):
+ if self.intResolution is None:
+ return ["--off"]
+ args = ["--mode", self.intResolution.forXrandr()] # set internal screen to desired resolution
+ if not self.extIsPrimary:
+ args.append('--primary')
+ return args
+
+ def getExternalArgs(self, intName):
+ if self.extResolution is None:
+ return ["--off"]
+ args = ["--mode", self.extResolution.forXrandr()] # set external screen to desired resolution
+ if self.extIsPrimary:
+ args.append('--primary')
+ if self.intResolution is None:
+ return args
+ # set position
+ args += [{
+ RelativeScreenPosition.LEFT : '--left-of',
+ RelativeScreenPosition.RIGHT : '--right-of',
+ RelativeScreenPosition.ABOVE : '--above',
+ RelativeScreenPosition.BELOW : '--below',
+ RelativeScreenPosition.MIRROR: '--same-as',
+ }[self.relPosition], intName]
+ return args
+
+
+class ScreenSituation:
+ connectors = {} # maps connector names to lists of Resolution (empty list -> disabled connector)
+ internalConnector = None # name of the internal connector (will be an enabled one)
+ externalConnector = None # name of the used external connector (an enabled one), or None
+
+ '''Represents the "screen situation" a machine can be in: Which connectors exist, which resolutions do they have, what are the names for the internal and external screen'''
+ def __init__(self, internalConnectorNames, externalConnectorNames = None):
+ '''Both arguments are lists of connector names. The first one which exists and has a screen attached is chosen for that class. <externalConnectorNames> can be None to
+ just choose any remaining connector.'''
+ # which connectors are there?
+ self._getXrandrInformation()
+ print(self.connectors)
+ # figure out which is the internal connector
+ self.internalConnector = self._findAvailableConnector(internalConnectorNames)
+ if self.internalConnector is None:
+ raise Exception("Could not automatically find internal connector, please use ~/.dsl.conf to specify it manually.")
+ print(self.internalConnector)
+ # and the external one
+ if externalConnectorNames is None:
+ externalConnectorNames = list(self.connectors.keys())
+ externalConnectorNames.remove(self.internalConnector)
+ self.externalConnector = self._findAvailableConnector(externalConnectorNames)
+ print(self.externalConnector)
+
+ # Run xrandr and fill the dict of connector names mapped to lists of available resolutions.
+ def _getXrandrInformation(self):
+ connector = None # current connector
+ for line in processOutputGen("xrandr", "-q"):
+ # screen?
+ m = re.search(r'^Screen [0-9]+: ', line)
+ if m is not None: # ignore this line
+ connector = None
+ continue
+ # new connector?
+ m = re.search(r'^([\w\-]+) (dis)?connected ', line)
+ if m is not None:
+ connector = m.groups()[0]
+ assert connector not in self.connectors
+ self.connectors[connector] = []
+ continue
+ # new resolution?
+ m = re.search(r'^ ([\d]+)x([\d]+) +', line)
+ if m is not None:
+ resolution = Resolution(int(m.groups()[0]), int(m.groups()[1]))
+ assert connector is not None
+ self.connectors[connector].append(resolution)
+ continue
+ # unknown line
+ # not fatal, e.g. xrandr shows strange stuff when a display is enabled, but not connected
+ print("Warning: Unknown xrandr line %s" % line)
+
+ # return the first available connector from those listed in <tryConnectors>, skipping disabled connectors
+ def _findAvailableConnector(self, tryConnectors):
+ for connector in tryConnectors:
+ if connector in self.connectors and len(self.connectors[connector]): # if the connector exists and is active (i.e. there is a resolution)
+ return connector
+ return None
+
+ # return available internal resolutions
+ def internalResolutions(self):
+ return self.connectors[self.internalConnector]
+
+ # return available external resolutions (or None, if there is no external screen connected)
+ def externalResolutions(self):
+ if self.externalConnector is None:
+ return None
+ return self.connectors[self.externalConnector]
+
+ # return resolutions available for both internal and external screen
+ def commonResolutions(self):
+ internalRes = self.internalResolutions()
+ externalRes = self.externalResolutions()
+ assert externalRes is not None
+ return [res for res in externalRes if res in internalRes]
+
+ # compute the xrandr call
+ def forXrandr(self, setup):
+ # turn all screens off
+ connectorArgs = {} # maps connector names to xrand arguments
+ for c in self.connectors.keys():
+ connectorArgs[c] = ["--off"]
+ # set arguments for the relevant ones
+ connectorArgs[self.internalConnector] = setup.getInternalArgs()
+ if self.externalConnector is not None:
+ connectorArgs[self.externalConnector] = setup.getExternalArgs(self.internalConnector)
+ else:
+ assert setup.extResolution is None, "There's no external screen to set a resolution for"
+ # now compose the arguments
+ call = ["xrandr"]
+ for name in connectorArgs:
+ call += ["--output", name] + connectorArgs[name]
+ return call
+
# DSL - easy Display Setup for Laptops
-# Copyright (C) 2012 Ralf Jung <post@ralfj.de>
+# Copyright (C) 2012 Ralf Jung <post@ralfj.de>
+# Copyright (C) 2012-2015 Constantin Berhard<constantin@exxxtremesys.lu>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-from dsl import RelativeScreenPosition, ScreenSetup, res2user, processOutputIt
+from screen import RelativeScreenPosition, ScreenSetup, processOutputIt
def userChoose (title, choices, returns, fallback):
assert len(choices) == len(returns)
extres = externalResolutions[0]
extprim = None
if relpos != RelativeScreenPosition.EXTERNAL_ONLY:
- intres = userChoose ("internal display resolution", list(map(res2user,internalResolutions)), internalResolutions, None)
+ intres = userChoose ("internal display resolution", list(map(str,internalResolutions)), internalResolutions, None)
if intres == None:
return None
else:
extprim = True
- extres = userChoose ("external display resolution", list(map(res2user,externalResolutions)), externalResolutions, None)
+ extres = userChoose ("external display resolution", list(map(str,externalResolutions)), externalResolutions, None)
if extres == None:
return None
if extprim == None:
extprim = userChoose ("Which display should be the primary display?", ["internal display", "external display"], [False, True], None)
if extprim == None:
return None
- return ScreenSetup(relpos,intres,extres,extprim)
+ return ScreenSetup(intres,extres,relpos,extprim)