- The tri-state --show actually did not make that much sense, which was particualrily obvious because of questions like
"What should the default mode be?". We have -r/-e/-i for that. So replace --show by --silent, such that e.g.,
`lilass -s -r mirror` will use the saved data and fall back to mirror mode, never showing a UI under normal circumstances.
On the other hand, `lilass -s` *will* show a UI for unknown screens only.
- Rename last* -> previous*
- Sort resolutions by size only (not depending on preferred/previous resolution)
- Qt UI: pre-select previous/preferred resolution
- Remove some Debug output
from enum import Enum
import gui, screen, util, database
frontend = gui.getFrontend("cli") # the fallback, until we got a proper frontend. This is guaranteed to be available.
-
+cmdArgs = None
# for auto-config: common names of internal connectors
commonInternalConnectorPrefixes = ['LVDS', 'eDP']
help="The frontend to be used for user interaction")
parser.add_argument("-r", "--relative-position",
dest="rel_position", choices=list(map(relPosFilter, screen.RelativeScreenPosition.__members__.keys())),
- help="Set the position of external screen relative to internal one.")
+ help="Set the position of external screen relative to internal one, in case it is not found in the DB.")
parser.add_argument("-e", "--external-only",
dest="external_only", action='store_true',
help="If an external screen is connected, disable all the others.")
parser.add_argument("-i", "--internal-only",
dest="internal_only", action='store_true',
help="Enable internal screen, disable all the others.")
- parser.add_argument("-s", "--show",
- dest="show", choices=ShowLevels.getNames(), default=ShowLevels.ONEXTERNAL.text,
- help="In which situations should the UI be displayed?")
+ parser.add_argument("-s", "--silent",
+ dest="silent", action='store_true',
+ help="Prefer to be silent: Opens a UI only if the external screen is not known *and* no default configuration (-r/-e/-i) is given.")
+ parser.add_argument("-v", "--verbose",
+ dest="verbose", action='store_true',
+ help="More verbose output on stderr.")
cmdArgs = parser.parse_args()
# load frontend early (for error mssages)
# see what situation we are in
situation = situationByConfig(config)
- # fetch info from the database
- # if it is too slow to open the DB twice (reading and saving), we can keep it open all the time
- with database.Database(databaseFilePath) as db:
- situation.fetchDBInfo(db)
-
# construct the ScreenSetup
setup = None
- if not cmdArgs.internal_only and situation.externalResolutions() is not None:
- # there's an external screen connected that we may want to use
- if cmdArgs.external_only:
- setup = screen.ScreenSetup(intResolution = None, extResolution = situation.externalResolutions()[0])
+ if situation.externalConnector is not None:
+ # There's an external screen connected that we may want to use.
+ # Fetch info about this screen from the database.
+ # NOTE: If it is too slow to open the DB twice (reading and saving), we can keep it open all the time
+ with database.Database(databaseFilePath) as db:
+ situation.fetchDBInfo(db)
+ # what to we do?
+ have_default_conf = bool(cmdArgs.external_only or cmdArgs.internal_only or cmdArgs.rel_position)
+ no_ui = bool(have_default_conf or (situation.previousSetup and cmdArgs.silent))
+ if not no_ui:
+ # ask the user what to do
+ setup = frontend.setup(situation)
+ if setup is None: sys.exit(1) # the user canceled
+ with database.Database(databaseFilePath) as db:
+ situation.putDBInfo(db, setup)
+ elif situation.previousSetup:
+ # apply the old setup again
+ setup = situation.previousSetup
+ # use default config from CLI
+ elif cmdArgs.external_only:
+ setup = screen.ScreenSetup(intResolution = None, extResolution = situation.externalConnector.getPreferredResolution())
elif cmdArgs.rel_position is not None:
# construct automatically, based on CLI arguments
# first, figure out the desired RelativeScreenPosition... waht a bad hack...
res = situation.commonResolutions()[0]
setup = screen.ScreenSetup(res, res, relPos)
else:
- setup = screen.ScreenSetup(intResolution = situation.internalResolutions()[0], extResolution = situation.externalResolutions()[0], relPosition = relPos)
- else:
- showlvl = ShowLevels(cmdArgs.show)
- if showlvl != ShowLevels.ONEXTERNAL and situation.lastSetup:
- # use last config
- setup = situation.lastSetup
- elif showlvl == ShowLevels.ONERROR:
- # guess config
- setup = screen.ScreenSetup(situation.internalResolutions()[0], situation.externalResolutions()[0], screen.RelativeScreenPosition.RIGHT)
- # TODO make default relative position configurable in the config file
- # TODO this has a bit of code duplication with the cmdArgs method above
- else:
- # ask the user
- setup = frontend.setup(situation)
- if setup is None: sys.exit(1) # the user canceled
- with database.Database(databaseFilePath) as db:
- situation.putDBInfo(db, setup)
- else:
- # use first resolution of internal connector
- setup = screen.ScreenSetup(intResolution = situation.internalConnector.getResolutionList()[0], extResolution = None)
+ setup = screen.ScreenSetup(intResolution = situation.internalConnector.getPreferredResolution(),
+ extResolution = situation.externalConnector.getPreferredResolution(),
+ relPosition = relPos)
+ # cmdArgs.internal_only: fall-through
+ if setup is None:
+ assert cmdArgs.internal_only or situation.externalConnector is None
+ # Nothing chosen yet? Use first resolution of internal connector.
+ setup = screen.ScreenSetup(intResolution = situation.internalConnector.getPreferredResolution(), extResolution = None)
# call xrandr
xrandrCall = situation.forXrandr(setup)
if setup.extResolution is None:
turnOnBacklight()
except Exception as e:
- raise e
frontend.error(str(e))
+ if cmdArgs is None or cmdArgs.verbose:
+ raise(e)
syncIfMirror(self.intRes, self.extRes)
syncIfMirror(self.extRes, self.intRes)
+ # if situation has a previousSetup, use its values as initial state
+ if situation.previousSetup:
+ p = situation.previousSetup
+ self.intEnabled.setChecked(p.intResolution is not None)
+ self.extEnabled.setChecked(p.extResolution is not None)
+ if p.relPosition:
+ self.relPos.setCurrentIndex(p.relPosition.value - 1)
+ if p.extIsPrimary:
+ self.extPrimary.setChecked(True)
+ else:
+ self.intPrimary.setChecked(True)
+ # Pre-select the previous resolution
+ self._intDefaultRes = p.intResolution
+ self._extDefaultRes = p.extResolution
+ self._mirrorDefaultRes = p.intResolution if p.relPosition == RelativeScreenPosition.MIRROR else None # in case of a mirror, they would be the same anyway
+ else:
+ self._intDefaultRes = situation.internalConnector.getPreferredResolution()
+ self._extDefaultRes = situation.externalConnector.getPreferredResolution()
+ self._mirrorDefaultRes = None
+
# connect the update function
self.intEnabled.toggled.connect(self.updateEnabledControls)
self.extEnabled.toggled.connect(self.updateEnabledControls)
self.relPos.currentIndexChanged.connect(self.updateEnabledControls)
- # if situation has a lastSetup, use its values as initial state
- if situation.lastSetup:
- last = situation.lastSetup
- self.intEnabled.setChecked(last.intResolution is not None)
- self.extEnabled.setChecked(last.extResolution is not None)
- if last.relPosition:
- self.relPos.setCurrentIndex(last.relPosition.value-1)
-
# make sure we are in a correct state
self.updateEnabledControls()
idx = self.relPos.currentIndex()
return self.relPos.itemData(idx)
- def fillResolutionBox(self, box, resolutions):
+ def fillResolutionBox(self, box, resolutions, select = None):
# 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)
+ if res == select:
+ box.setCurrentIndex(idx)
else:
# first clear it
while box.count() > 0:
# then fill it
for res in resolutions:
box.addItem(str(res), res)
+ if res == select:
+ box.setCurrentIndex(box.count() - 1) # select the most recently added one
def updateEnabledControls(self):
intEnabled = self.intEnabled.isChecked()
# which resolutions do we offer?
if self.isMirror:
commonRes = self._situation.commonResolutions()
- self.fillResolutionBox(self.intRes, commonRes)
- self.fillResolutionBox(self.extRes, commonRes)
+ self.fillResolutionBox(self.intRes, commonRes, select = self._mirrorDefaultRes)
+ self.fillResolutionBox(self.extRes, commonRes, select = self._mirrorDefaultRes)
self.intRes.setCurrentIndex(self.extRes.currentIndex())
else:
- self.fillResolutionBox(self.intRes, self._situation.internalResolutions())
- self.fillResolutionBox(self.extRes, self._situation.externalResolutions())
+ self.fillResolutionBox(self.intRes, self._situation.internalConnector.getResolutionList(), select = self._intDefaultRes)
+ self.fillResolutionBox(self.extRes, self._situation.externalConnector.getResolutionList(), select = self._extDefaultRes)
# configure position control
self.posGroup.setEnabled(bothEnabled)
self.posLabel1.setEnabled(bothEnabled)
return self.userChoose("Select resolution for %s"%displayname, modedescs, availablemodes, None)
def setup (self, situation):
- if situation.lastSetup:
- applyLast = self.userChoose("This display is known. The last setup for it was like this:\n%s.\nApply the last used configuration?" % str(situation.lastSetup), ("Apply last setup", "Enter different setup"), (True,False), None)
- if applyLast is None:
+ if situation.previousSetup:
+ applyPrevious = self.userChoose("This display is known. The last setup for it was like this:\n%s.\nApply the last used configuration?" % str(situation.previousSetup), ("Apply last setup", "Enter different setup"), (True,False), None)
+ if applyPrevious is None:
return None
- if applyLast is True:
- return situation.lastSetup
- assert applyLast is False
+ if applyPrevious is True:
+ return situation.previousSetup
+ assert applyPrevious is False
operationmodes = list(OperationMode)
operationmodedescs = list(map(lambda x: x.text, operationmodes))
operationmode = self.userChoose ("Display setup", operationmodedescs, operationmodes, None)
def __init__(self, name=None):
self.name = name # connector name, e.g. "HDMI1"
self.edid = None # EDID string for the connector, or None if disconnected
- self._resolutions = set() # list of Resolution objects, empty if disconnected
- self.preferredResolution = None
- self.__lastResolution = None
+ self._resolutions = set() # set of Resolution objects, empty if disconnected
+ self._preferredResolution = None
+ self.previousResolution = None
self.hasLastResolution = False
def __str__(self):
def __repr__(self):
return """<Connector "%s" EDID="%s" resolutions="%s">""" % (str(self.name), str(self.edid), ", ".join(str(r) for r in self.getResolutionList()))
- def __setLastRes(self, res):
- # res == None means this display was last switched off
- if res is not None and not res in self._resolutions:
- raise ValueError("Resolution "+res+" not available for "+self.name+".")
- self.__lastResolution = res
- self.hasLastResolution = True
-
- def __getLastRes(self):
- if not self.hasLastResolution:
- raise ValueError("Connector %s has no last known resolution." % self.name)
- return self.__lastResolution
-
- lastResolution = property(__getLastRes, __setLastRes)
-
def isConnected(self):
- assert (self.edid is None) == (len(self._resolutions)==0)
+ assert (self.edid is None) == (len(self._resolutions)==0), "Resolution-EDID mismatch; #resolutions: {}".format(len(self._resolutions))
return self.edid is not None
def addResolution(self, resolution):
else:
self.edid += s
+ def setPreferredResolution(self, resolution):
+ assert isinstance(resolution, Resolution) and resolution in self._resolutions
+ self._preferredResolution = resolution
+
+ def getPreferredResolution(self):
+ if self._preferredResolution is not None:
+ return self._preferredResolution
+ return self.getResolutionList()[0] # prefer the largest resolution
+
def getResolutionList(self):
- return sorted(self._resolutions, key=lambda r: (0 if self.hasLastResolution and r==self.lastResolution else 1, 0 if r==self.preferredResolution else 1, -r.pixelCount()))
+ return sorted(self._resolutions, key=lambda r: -r.pixelCount())
class ScreenSituation:
connectors = [] # contains all the Connector objects
internalConnector = None # the internal Connector object (will be an enabled one)
externalConnector = None # the used external Connector object (an enabled one), or None
+ previousSetup = None # None or the ScreenSetup used the last time this external screen was connected
'''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):
just choose any remaining connector.'''
# which connectors are there?
self._getXrandrInformation()
- for c in self.connectors:
- print(repr(c))
- print()
# figure out which is the internal connector
self.internalConnector = self._findAvailableConnector(internalConnectorNames)
if self.internalConnector is None:
if self.internalConnector == self.externalConnector:
raise Exception("Internal and external connector are the same. This must not happen. Please fix ~/.dsl.conf.");
print("Detected external connector:",self.externalConnector)
- # self.lastSetup is left uninitialized so you can't access it before trying a lookup in the database
# Run xrandr and fill the dict of connector names mapped to lists of available resolutions.
def _getXrandrInformation(self):
if m is not None:
connector = Connector(m.group(1))
assert not any(c.name == connector.name for c in self.connectors)
- self.connectors.append(connector)
+ if not connector.name.startswith("VIRTUAL"):
+ # skip "VIRTUAL" connectors
+ self.connectors.append(connector)
continue
# new resolution?
m = re.search(r'^\s*([\d]+)x([\d]+)', line)
resolution = Resolution(int(m.group(1)), int(m.group(2)))
assert connector is not None
connector.addResolution(resolution)
- if '+preferred' in line:
- connector.preferredResolution = resolution
+ if re.search(r' [+]preferred\b', line):
+ connector.setPreferredResolution(resolution)
continue
# EDID?
m = re.search(r'^\s*EDID:\s*$', line)
readingEdid = True
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)
+ # not fatal, e.g. xrandr shows strange stuff when a display is enabled, but not connected; --verbose adds a whole lot of other weird stuff
# return the first available connector from those listed in <tryConnectorNames>, skipping disabled connectors
def _findAvailableConnector(self, tryConnectorNames):
return c
return None
- # return available internal resolutions
- def internalResolutions(self):
- return self.internalConnector.getResolutionList()
-
- # 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.externalConnector.getResolutionList()
-
# return resolutions available for both internal and external screen
def commonResolutions(self):
- internalRes = self.internalResolutions()
- externalRes = self.externalResolutions()
- assert externalRes is not None
+ assert self.externalConnector is not None, "Common resolutions may only be queried if there is an external screen connected."
+ internalRes = self.internalConnector.getResolutionList()
+ externalRes = self.externalConnector.getResolutionList()
return sorted(set(externalRes).intersection(internalRes), key=lambda r: -r.pixelCount())
# compute the xrandr call
def fetchDBInfo(self, db):
if self.externalConnector and self.externalConnector.edid:
- self.lastSetup = db.getConfig(self.externalConnector.edid) # may also return None
- else:
- self.lastSetup = None
- if self.lastSetup:
- print("SETUP FOUND", self.lastSetup)
- self.externalConnector.lastResolution = self.lastSetup.extResolution
- self.internalConnector.lastResolution = self.lastSetup.intResolution
+ self.previousSetup = db.getConfig(self.externalConnector.edid) # may also return None
else:
- print("NO SETUP FOUND")
+ self.previousSetup = None
+ if self.previousSetup:
+ print("Known screen, previous setup:", self.previousSetup)
+ self.externalConnector.previousResolution = self.previousSetup.extResolution
+ self.internalConnector.previousResolution = self.previousSetup.intResolution
def putDBInfo(self, db, setup):
if not self.externalConnector or not self.externalConnector.edid: