saving to and loading from db works :-)
authorConstantin Berhard <git.mail.enormator@xoxy.net>
Tue, 24 Nov 2015 00:33:34 +0000 (01:33 +0100)
committerConstantin Berhard <git.mail.enormator@xoxy.net>
Tue, 24 Nov 2015 00:33:34 +0000 (01:33 +0100)
database.py [new file with mode: 0644]
lilass
screen.py

diff --git a/database.py b/database.py
new file mode 100644 (file)
index 0000000..c65b4b9
--- /dev/null
@@ -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 85ccc99be2687a32d5a9fa8057047290f2f4185b..42c15c67775b08774cfb8d3ba3a73f10a1300079 100755 (executable)
--- a/lilass
+++ b/lilass
@@ -18,7 +18,7 @@
 
 import argparse, sys, os, os.path, shutil, re, subprocess
 from enum import Enum
 
 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.
 
 
 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'))
 
     # 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__":
 
 # 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("-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)
         
         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")
         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)
             # 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)
         
         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:
         # 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
                 # 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)
         else:
             # use first resolution of internal connector
             setup = screen.ScreenSetup(intResolution = situation.internalConnector.getResolutionList()[0], extResolution = None)
index b0c657737403be53b1a05ec7b4edef227b76f19c..39c9367d6b61291bfab410c9318755f194ccf241 100644 (file)
--- a/screen.py
+++ b/screen.py
@@ -45,7 +45,7 @@ class RelativeScreenPosition(Enum):
     def __init__(self, text):
         # auto numbering
         cls = self.__class__
     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.text = text
 
 class Resolution:
@@ -54,6 +54,24 @@ class Resolution:
         self.width = width
         self.height = height
     
         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 __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 pixelCount(self):
         return self.width * self.height
-    
-    def forXrandr(self):
-        return str(self.width)+'x'+str(self.height)
 
 
 class ScreenSetup:
 
 
 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.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 __str__(self):
         return str(self.name)
@@ -133,6 +150,20 @@ class Connector:
     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 __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
     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):
             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
 
 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)
         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):
     
     # 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
 
             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)