From 381dd16b7011170fac8413d93f0d175ef2f22c7a Mon Sep 17 00:00:00 2001 From: Constantin Berhard Date: Tue, 24 Nov 2015 01:33:34 +0100 Subject: [PATCH 1/1] saving to and loading from db works :-) --- database.py | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++ lilass | 33 ++++++++++++++++++++++-- screen.py | 58 ++++++++++++++++++++++++++++++++++++++---- 3 files changed, 156 insertions(+), 7 deletions(-) create mode 100644 database.py diff --git a/database.py b/database.py new file mode 100644 index 0000000..c65b4b9 --- /dev/null +++ b/database.py @@ -0,0 +1,72 @@ +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() diff --git a/lilass b/lilass index 85ccc99..42c15c6 100755 --- a/lilass +++ b/lilass @@ -18,7 +18,7 @@ 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. @@ -78,6 +78,18 @@ def situationByConfig(config): # 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__": @@ -99,12 +111,16 @@ 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") @@ -112,11 +128,22 @@ if __name__ == "__main__": # 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: @@ -139,6 +166,8 @@ if __name__ == "__main__": # 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) diff --git a/screen.py b/screen.py index b0c6577..39c9367 100644 --- a/screen.py +++ b/screen.py @@ -45,7 +45,7 @@ class RelativeScreenPosition(Enum): 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: @@ -54,6 +54,24 @@ 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 @@ -76,9 +94,6 @@ class Resolution: def pixelCount(self): return self.width * self.height - - def forXrandr(self): - return str(self.width)+'x'+str(self.height) class ScreenSetup: @@ -126,6 +141,8 @@ class Connector: 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) @@ -133,6 +150,20 @@ class Connector: def __repr__(self): return """""" % (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 @@ -148,7 +179,7 @@ class Connector: 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 @@ -177,6 +208,7 @@ class ScreenSituation: 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): @@ -262,3 +294,19 @@ class ScreenSituation: 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) -- 2.30.2