Exemplo n.º 1
0
class Disper:

    # static information
    name = "disper"
    version = "0.3.0"
    prefix = build.prefix
    prefix_share = build.prefix_share

    # option parsing
    argv = []
    parser = None  # option parser object
    options = None  # parsed options
    args = None  # parsed arguments

    # real work
    switcher = None  # switcher object
    plugins = None  # plugins object
    log = None

    def __init__(self):
        self.log = logging.getLogger("disper")
        self.log.setLevel(logging.WARNING)
        self._options_init()
        self.plugins = Plugins(self)
        # self.plugins.call('init') # can't really do here since list of plugins isn't read yet
        self.switcher = Switcher()
        # add default options
        # TODO do initial parsing too so errors can be traced to config
        conffile = os.path.join(os.getenv("HOME"), ".disper", "config")
        if os.path.exists(conffile):
            f = open(conffile, "r")
            opts = ""
            for l in f.readlines():
                opts += l.split("#", 1)[0] + " "
            f.close()
            self.options_append(shlex.split(opts))

    def _options_init(self):
        """initialize default command-line options"""
        usage = "usage: %prog [options] (-l|-s|-c|-e|-p|-i)"
        version = " ".join(map(str, [self.name, self.version]))
        self.parser = optparse.OptionParser(usage=usage, version=version)

        self.add_option(
            "-v", "--verbose", action="store_const", dest="debug", const=logging.INFO, help="show what's happening"
        )
        self.add_option(
            "-q",
            "--quiet",
            action="store_const",
            dest="debug",
            const=logging.ERROR,
            help="be quiet and only show errors",
        )
        self.add_option(
            "-r",
            "--resolution",
            dest="resolution",
            help='set resolution, e.g. "800x600", or "auto" to detect the display\'s preferred '
            + 'resolution, or "max" to use the maximum resolution advertised. For extend it '
            + "is possible to enter a single resolution for all displays or a comma-separated "
            + "list of resolutions (one for each display). Beware that many displays advertise "
            + 'resolutions they can not fully show, so "max" is not advised.',
        )
        self.add_option(
            "-d",
            "--displays",
            dest="displays",
            help='comma-separated list of displays to operate on, or "auto" to detect; '
            + "the first is the primary display.",
        )
        self.add_option(
            "-t",
            "--direction",
            dest="direction",
            choices=["left", "right", "top", "bottom"],
            help='where to extend displays: "left", "right", "top", or "bottom"',
        )
        self.add_option(
            "",
            "--scaling",
            dest="scaling",
            choices=["default", "native", "scaled", "centered", "aspect-scaled"],
            help='flat-panel scaling mode: "default", "native", "scaled", "centered", or "aspect-scaled"',
        )
        self.add_option(
            "",
            "--plugins",
            dest="plugins",
            help='comma-separated list of plugins to enable. Special names: "user" for all user plugins '
            + 'in ~/.disper/hooks; "all" for all plugins found; "none" for no plugins.',
        )
        self.add_option(
            "",
            "--cycle-stages",
            dest="cycle_stages",
            help="colon-separated list command-line arguments to cycle through",
        )

        group = optparse.OptionGroup(self.parser, "Actions", "Select exactly one of the following actions")
        self._add_option(
            group,
            "-l",
            "--list",
            action="append_const",
            const="list",
            dest="actions",
            help="list the attached displays",
        )
        self._add_option(
            group,
            "-s",
            "--single",
            action="append_const",
            const="single",
            dest="actions",
            help="only enable the primary display",
        )
        self._add_option(
            group,
            "-S",
            "--secondary",
            action="append_const",
            const="secondary",
            dest="actions",
            help="only enable the secondary display",
        )
        self._add_option(
            group, "-c", "--clone", action="append_const", const="clone", dest="actions", help="clone displays"
        )
        self._add_option(
            group, "-e", "--extend", action="append_const", const="extend", dest="actions", help="extend displays"
        )
        self._add_option(
            group,
            "-p",
            "--export",
            action="append_const",
            const="export",
            dest="actions",
            help="export current settings to standard output",
        )
        self._add_option(
            group,
            "-i",
            "--import",
            action="append_const",
            const="import",
            dest="actions",
            help="import current settings from standard input",
        )
        self._add_option(
            group,
            "-C",
            "--cycle",
            action="append_const",
            const="cycle",
            dest="actions",
            help="cycle through the list of cycle stages",
        )
        self.parser.add_option_group(group)

    def add_option(self, *args, **kwargs):
        """adds an option to the parser. Implements append_const for Python<2.5 too"""
        return self._add_option(self.parser, *args, **kwargs)

    def _add_option(self, obj, *args, **kwargs):
        """portable optarg add_option function that implements the append_const
        action for Python versions below 2.5; has an extra first argument as
        the object on which add_option should be called."""
        if sys.hexversion < 0x020500F0 and "action" in kwargs and kwargs["action"] == "append_const":
            # after: http://permalink.gmane.org/gmane.comp.python.optik.user/284
            def append_const_cb(const):
                def cb(opt, opt_str, value, parser):
                    if not getattr(parser.values, opt.dest):
                        setattr(parser.values, opt.dest, list())
                    getattr(parser.values, opt.dest).append(const)

                return cb

            kwargs["action"] = "callback"
            kwargs["callback"] = append_const_cb(kwargs["const"])
            del kwargs["const"]
        return obj.add_option(*args, **kwargs)

    def options_append(self, args):
        """parses command-line options; can be called multiple times"""
        self.argv += args

    def options_parse(self, args=None):
        """parses command-line options given; adds options to current list if set"""
        if args:
            self.options_append(args)
        (self.options, self.args) = self.parser.parse_args(self.argv)
        # need exactly one action
        if not self.options.actions:
            self.options.actions = []
        elif len(self.options.actions) > 1:
            self.parser.error(
                "conflicting actions, please specify exactly one action: " + ", ".join(self.options.actions)
            )
            raise SystemExit(2)

        if "import" in self.options.actions or "export" in self.options.actions:
            if self.options.resolution:
                self.log.warning("specified resolution ignored for %s" % self.options.actions[0])
            if self.options.displays:
                self.log.warning("specified displays ignored for %s" % self.options.actions[0])

        # apply defaults here to be able to detect if they were set explicitly or not
        if not self.options.direction:
            self.options.direction = "right"
        if not self.options.resolution:
            self.options.resolution = "auto"
        if not self.options.displays:
            self.options.displays = "auto"
        if not self.options.scaling:
            self.options.scaling = "default"
        if not self.options.debug:
            self.options.debug = logging.WARNING
        if self.options.plugins == None:
            self.options.plugins = "user"
        self.log.setLevel(self.options.debug)
        self.options.plugins = map(lambda x: x.strip(), self.options.plugins.split(","))
        if self.options.displays != "auto":
            self.options.displays = map(lambda x: x.strip(), self.options.displays.split(","))
        if self.options.resolution not in ["auto", "max"]:
            self.options.resolution = map(lambda x: x.strip(), self.options.resolution.split(","))
        self.plugins.set_enabled(self.options.plugins)

    def switch(self):
        """Switch to configuration as specified in the options"""
        if len(self.options.actions) == 0:
            self.log.info("no action specified")
            # show help if no action specified
            self.parser.print_help()
            raise SystemExit(2)
        if "single" in self.options.actions:
            if self.options.displays != "auto":
                self.log.warning("specified displays ignored for single")
            self.switch_primary()
        elif "secondary" in self.options.actions:
            if self.options.displays != "auto":
                self.log.warning("specified displays ignored for secondary")
            self.switch_secondary()
        elif "clone" in self.options.actions:
            self.switch_clone()
        elif "extend" in self.options.actions:
            self.switch_extend()
        elif "export" in self.options.actions:
            print self.export_config()
        elif "import" in self.options.actions:
            self.import_config("\n".join(sys.stdin))
        elif "cycle" in self.options.actions:
            self._cycle(self.options.cycle_stages.split(":"))
        elif "list" in self.options.actions:
            # list displays with resolutions
            displays = self.options.displays
            if displays == "auto":
                displays = self.switcher.get_displays()
            for disp in displays:
                res = self.switcher.get_resolutions_display(disp)
                res.sort()
                print "display %s: %s" % (disp, self.switcher.get_display_name(disp))
                print " resolutions: " + str(res)
        else:
            self.log.critical("program error, unrecognised action: " + ", ".join(self.options.actions))
            raise SystemExit(2)

    def switch_primary(self, res=None):
        """Only enable primary display.
           @param res resolution to use; or 'auto' for default or None for option"""
        return self.switch_single(self.switcher.get_primary_display())

    def switch_secondary(self, res=None):
        """Only enable secondary display.
           @param res resolution to use; or 'auto' for default or None for option"""
        primary = self.switcher.get_primary_display()
        try:
            display = [x for x in self.switcher.get_displays() if x != primary][0]
        except IndexError:
            self.log.critical("No secondary display found, falling back to primary.")
            return self.switch_single(primary, res)
        return self.switch_single(display, res)

    def switch_single(self, display=None, res=None):
        """Only enable one display.
           @param display display to enable; or 'auto' for primary or None for option
           @param res resolution to use; or 'auto' for default or None for option"""
        if not display:
            display = self.options.displays
        if display == "auto":
            display = self.switcher.get_primary_display()
        elif isinstance(display, list) and len(display) > 1:
            self.log.warning("single output requested but multiple specified; using first one")
            display = display[0]
        if display:
            display = [display]
        return self.switch_clone(display, res)

    def switch_clone(self, displays=None, res=None):
        """Clone displays.
           @param displays list of displays; or 'auto' for default or None for option
           @param res resolution; or 'auto' for default, 'max' for max or None for option"""
        # figure out displays
        if not displays:
            displays = self.options.displays
        if displays == "auto":
            displays = self.switcher.get_displays()
            self.log.info("auto-detected displays: " + ", ".join(displays))
        else:
            self.log.info("using specified displays: " + ", ".join(displays))
        # figure out resolutions
        if not res:
            res = self.options.resolution
            if type(res) == list or type(res) == tuple:
                if len(res) != 1:
                    raise TypeError("need single resolution for clone")
                res = res[0]
        if res == "auto" or res == "max":
            r = self.switcher.get_resolutions(displays).common()
            if len(r) == 0:
                self.log.critical("displays share no common resolution")
                raise SystemExit(1)
            if res == "max":  # ignore any preferred resolution
                for s in r:
                    s.weight = 0
            res = sorted(r)[-1]
        else:
            res = Resolution(res)
        # and switch
        result = self.switcher.switch_clone(displays, res)
        self.plugins.set_layout_clone(displays, res)
        self.plugins.call("switch")
        return result

    def switch_extend(self, displays=None, direction=None, ress=None):
        """Extend displays.
           @param displays list of displays; or 'auto for default or None for option
           @param direction direction to extend; or None for option
           @param ress list of resolutions; or 'auto' for default or 'max' for max or None for option"""
        # figure out displays
        if not displays:
            displays = self.options.displays
        if displays == "auto":
            displays = self.switcher.get_displays()
            self.log.info("auto-detected displays: " + ", ".join(displays))
        else:
            self.log.info("using specified displays: " + ", ".join(displays))
        # figure out resolutions
        if not ress:
            ress = self.options.resolution
        if ress == "max":  # max resolution for each
            # override auto-detection weights and get highest resolution
            ress = self.switcher.get_resolutions(displays)
            for rl in ress.values():
                for r in rl:
                    r.weight = 0
            ress = ress.select()
            self.log.info("maximum resolutions for displays: " + str(ress))
        elif ress == "auto":  # use preferred resolution for each
            ress = self.switcher.get_resolutions(displays).select()
            self.log.info("preferred resolutions for displays: " + str(ress))
        else:  # list of resolutions specified
            ress = ResolutionSelection(ress, displays)
            if len(ress) == 1:
                ress = ress * len(displays)
            elif len(ress) != len(displays):
                self.log.critical(
                    'resolution: must specify either "auto", "max", a single value, or one for each display'
                )
                raise SystemExit(2)
            self.log.info("selected resolutions for displays: " + str(ress))
        # figure out direction
        if not direction:
            direction = self.options.direction
        # and switch
        result = self.switcher.switch_extend(displays, direction, ress)
        self.plugins.set_layout_extend(displays, direction, ress)
        self.plugins.call("switch")
        return result

    def export_config(self):
        return self.switcher.export_config()

    def import_config(self, data):
        result = self.switcher.import_config(data)
        self.plugins.call("switch")
        return result

    def _cycle(self, stages):
        # read last state
        stage = 0
        disperconf = os.path.join(os.getenv("HOME"), ".disper")
        statefile = os.path.join(disperconf, "last_cycle_stage")
        if os.path.exists(statefile):
            f = open(statefile, "r")
            stage = int(f.readline())
            f.close()
        # apply next
        stage += 1
        if stage >= len(stages):
            stage = 0
        self.argv = filter(lambda x: x != "-C" and x != "--cycle", self.argv)
        self.options_parse(shlex.split(stages[stage]))
        try:
            self.switch()
        finally:
            # write new state to file; do it here to make sure that a
            # failing configuration doesn't block the cycling
            if not os.path.exists(disperconf):
                os.mkdir(disperconf)
            f = open(statefile, "w")
            f.write(str(stage) + "\n")
            f.close()