add tests for parsing certain xrandr output; fix crash when screen doesn't have an...
[lilass.git] / lilass
diff --git a/lilass b/lilass
index 7b94e188e7aa5e5eecd9b0896854fcee551cbf65..3512a5fea3a1aeba5d4d3331741c6e9791a94448 100755 (executable)
--- a/lilass
+++ b/lilass
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 
-import argparse, sys, os, re, subprocess
+import argparse, sys, os, os.path, shutil, re, subprocess
 from enum import Enum
-import gui, screen
+import gui, screen, util, database
 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
-
+cmdArgs = None
 
 # Load a section-less config file: maps parameter names to space-separated lists of strings (with shell quotation)
 def loadConfigFile(filename):
@@ -75,11 +66,10 @@ def situationByConfig(config):
             raise Exception("You must specify exactly one internal connector.")
         internalConnectors = config['internalConnector']
     else:
-        internalConnectors = commonInternalConnectorNames()
+        internalConnectors = screen.commonInternalConnectorNames()
     # run!
     return screen.ScreenSituation(internalConnectors, config.get('externalConnectors'))
 
-
 # if we run top-level
 if __name__ == "__main__":
     try:
@@ -93,49 +83,92 @@ if __name__ == "__main__":
                             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", "--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("--no-db",
+                            dest="use_db", action='store_false',
+                            help="Do not use the database of known screens.")
+        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)
         frontend = gui.getFrontend(cmdArgs.frontend)
         
+        # find files
+        ## find config file
+        legacyConfigFilePath = os.getenv('HOME') + '/.lilass.conf'
+        configDirectory = util.getConfigDirectory()
+        configFilePath = os.path.join(configDirectory, "lilass.conf")
+        if os.path.isfile(legacyConfigFilePath) and not os.path.isfile(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(os.getenv('HOME') + '/.lilass.conf')
+        config = loadConfigFile(configFilePath)
         
         # see what situation we are in
         situation = situationByConfig(config)
         
         # 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
+            if cmdArgs.use_db:
+                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
+                if cmdArgs.use_db:
+                    # persists this to disk
+                    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...
+                # first, figure out the desired RelativeScreenPosition... what a bad hack...
                 relPos = list(filter(lambda relPosItem: relPosFilter(relPosItem[0]) == cmdArgs.rel_position, screen.RelativeScreenPosition.__members__.items()))
-                assert len(relPos) == 1, "CLI argument is ambigue"
+                assert len(relPos) == 1, "CLI argument is ambiguous"
                 relPos = relPos[0][1]
                 # now we construct the ScreenSetup
                 if relPos == screen.RelativeScreenPosition.MIRROR:
                     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:
-                # ask the user
-                setup = frontend.setup(situation)
-                if setup is None: sys.exit(1) # the user canceled
-        else:
-            # use first resolution of internal connector
-            setup = screen.ScreenSetup(intResolution = situation.internalResolutions()[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)
@@ -146,5 +179,6 @@ if __name__ == "__main__":
         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)