--- /dev/null
+import sqlite3
+import os.path
+from binascii import hexlify, unhexlify
+from screen import ScreenSetup, Resolution, RelativeScreenPosition
+
+class InvalidDBFile(Exception):
+ pass
+
+class Database:
+ def __init__(self, dbfilename):
+ self._create = False
+ assert(os.path.isdir(os.path.dirname(dbfilename)))
+ if not os.path.isfile(dbfilename):
+ if os.path.lexists(dbfilename):
+ raise Exception("Database must be a file: '%s'" % dbfilename)
+ # database will be created on __enter__ because we need a dbconnection for it
+ self._create = True
+ self._dbfilename = dbfilename
+ self._connection = None
+ def __enter__(self):
+ self._connection = sqlite3.connect(self._dbfilename)
+ c = self._c()
+ if self._create:
+ c.execute("""CREATE TABLE meta (key text, value text, PRIMARY KEY(key))""")
+ c.execute("""INSERT INTO meta VALUES ('version', '1')""")
+ c.execute("""CREATE TABLE known_configs (edid blob, resinternal text, resexternal text, mode text, ext_is_primary integer, PRIMARY KEY(edid))""")
+ # edid in binary format
+ # resindernal, resexternal = "1024x768" or NULL if display is off
+ # mode: the enum text of screen.RelativeScreenPosition or NULL if one display is off
+ else: # check compatibility
+ dbversion = int(self._getMeta("version"))
+ if dbversion > 1:
+ raise InvalidDBFile("Database is too new: Version %d. Please update lilass." % dbversion)
+ return self
+ def _getMeta(self, key):
+ c = self._c()
+ c.execute("""SELECT value FROM meta WHERE key=?""", (key,))
+ got = c.fetchone()
+ if got is None: # to differentiate between the value being NULL and the row being not there
+ raise KeyError("""Key "%s" is not in the meta table.""" % key)
+ assert c.fetchone() is None # uniqueness
+ assert len(got) == 1
+ return got[0]
+ def putConfig(self, extconn_edid, conf):
+ c = self._c()
+ b_edid = unhexlify(extconn_edid)
+ intres = conf.intResolution.forDatabase() if conf.intResolution else None
+ extres = conf.extResolution.forDatabase() if conf.extResolution else None
+ mode = conf.relPosition.text if conf.relPosition else None
+ extprim = int(conf.extIsPrimary) # False => 0, True => 1
+ c.execute("""INSERT OR REPLACE INTO known_configs VALUES (?,?,?,?,?)""", (b_edid, intres, extres, mode, extprim))
+ def getConfig(self, extconn_edid):
+ c = self._c()
+ b_edid = unhexlify(extconn_edid)
+ c.execute("""SELECT * FROM known_configs WHERE edid=?""", (b_edid,))
+ result = c.fetchone()
+ if result is None:
+ return None
+ assert c.fetchone() is None # uniqueness
+ _, intres, extres, mode, extprim = result
+ intres = Resolution.fromDatabase(intres) # this method is safe for NULLs
+ extres = Resolution.fromDatabase(extres)
+ mode = RelativeScreenPosition(mode)
+ extprim = bool(extprim) # 0 => False, 1 => True
+ return ScreenSetup(intres, extres, mode, extprim)
+ def __exit__(self, type, value, tb):
+ if self._connection:
+ self._connection.commit()
+ self._connection.close()
+ def _c(self):
+ assert(self._connection)
+ return self._connection.cursor()
import argparse, sys, os, os.path, shutil, re, subprocess
from enum import Enum
-import gui, screen, util
+import gui, screen, util, database
frontend = gui.getFrontend("cli") # the fallback, until we got a proper frontend. This is guaranteed to be available.
# run!
return screen.ScreenSituation(internalConnectors, config.get('externalConnectors'))
+class ShowLevels(Enum):
+ ONEXTERNAL = ("on-external")
+ ONNEW = ("on-new")
+ ONERROR = ("on-error")
+ def __init__(self, text):
+ # auto numbering
+ cls = self.__class__
+ self._value_ = len(cls.__members__) + 1
+ self.text = text
+ @classmethod
+ def getNames(cls):
+ return list(x.text for x in cls)
# if we run top-level
if __name__ == "__main__":
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(),
+ help="In which situations should the UI be displayed?")
cmdArgs = parser.parse_args()
# load frontend early (for error mssages)
frontend = gui.getFrontend(cmdArgs.frontend)
- # load configuration
+ # find files
+ ## find config file
legacyConfigFilePath = os.getenv('HOME') + '/.lilass.conf'
configDirectory = util.getConfigDirectory()
configFilePath = os.path.join(configDirectory, "lilass.conf")
# looks like we just upgraded to a new version of lilass
util.mkdirP(configDirectory)
shutil.move(legacyConfigFilePath, configFilePath)
+ ## find database
+ dataDirectory = util.getDataDirectory()
+ util.mkdirP(dataDirectory)
+ databaseFilePath = os.path.join(dataDirectory, "collected_data.sqlite")
+
+ # load configuration
config = loadConfigFile(configFilePath)
# 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:
# 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)
def __init__(self, text):
# auto numbering
cls = self.__class__
- self._value_ = len(cls.__members__)
+ self._value_ = len(cls.__members__) + 1
self.text = text
class Resolution:
self.width = width
self.height = height
+ @classmethod
+ def fromDatabase(cls, dbstr):
+ if dbstr is None:
+ return None
+ parts = dbstr.split("x")
+ if len(parts) != 2:
+ raise ValueError(xrandrstr)
+ return Resolution(*map(int,parts))
+
+ def forDatabase(self):
+ return str(self.width)+'x'+str(self.height)
+
+ def forXrandr(self):
+ return self.forDatabase()
+
+ def toTuple(self):
+ return (self.width, self.height)
+
def __eq__(self, other):
if not isinstance(other, Resolution):
return False
def pixelCount(self):
return self.width * self.height
-
- def forXrandr(self):
- return str(self.width)+'x'+str(self.height)
class ScreenSetup:
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.hasLastResolution = False
def __str__(self):
return str(self.name)
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)
return self.edid is not None
self.edid += s
def getResolutionList(self):
- return sorted(self._resolutions, key=lambda r: (0 if r==self.preferredResolution else 1, -r.pixelCount()))
+ 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()))
class ScreenSituation:
connectors = [] # contains all the Connector objects
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.preferredSetup 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):
call += ["--output", name] + connectorArgs[name]
return call
+ def fetchDBInfo(self, db):
+ if self.externalConnector and self.externalConnector.edid:
+ self.preferredSetup = db.getConfig(self.externalConnector.edid) # may also return None
+ else:
+ self.preferredSetup = None
+ if self.preferredSetup:
+ print("SETUP FOUND", self.preferredSetup)
+ self.externalConnector.lastResolution = self.preferredSetup.extResolution
+ self.internalConnector.lastResolution = self.preferredSetup.intResolution
+ else:
+ print("NO SETUP FOUND")
+
+ def putDBInfo(self, db, setup):
+ if not self.externalConnector or not self.externalConnector.edid:
+ return
+ db.putConfig(self.externalConnector.edid, setup)