~ubuntu-branches/ubuntu/saucy/disper/saucy-proposed

« back to all changes in this revision

Viewing changes to src/disper.py

  • Committer: Bazaar Package Importer
  • Author(s): Vincent Cheng
  • Date: 2011-07-17 22:30:03 UTC
  • Revision ID: james.westby@ubuntu.com-20110717223003-e000zieh4kgerk01
Tags: upstream-0.3.0
ImportĀ upstreamĀ versionĀ 0.3.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
###############################################################################
 
3
# disper.py - main disper
 
4
#
 
5
# This program is free software; you can redistribute it and/or modify
 
6
# it under the terms of the GNU General Public License as published by
 
7
# the Free Software Foundation; either version 3 of the License, or
 
8
# (at your option) any later version.
 
9
#        
 
10
# This program is distributed in the hope that it will be useful,
 
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 
13
# GNU General Public License at http://www.gnu.org/licenses/gpl.txt
 
14
# By using, editing and/or distributing this software you agree to
 
15
# the terms and conditions of this license.
 
16
 
 
17
import os
 
18
import sys
 
19
import logging
 
20
import optparse
 
21
import shlex
 
22
 
 
23
from switcher import Switcher, Resolution, ResolutionSelection
 
24
from plugins import Plugins
 
25
import build
 
26
 
 
27
# make sure to handle SystemExit when using this class in a 3rd-party program
 
28
 
 
29
class Disper:
 
30
 
 
31
    # static information
 
32
    name = 'disper'
 
33
    version = '0.3.0'
 
34
    prefix = build.prefix
 
35
    prefix_share = build.prefix_share
 
36
 
 
37
    # option parsing
 
38
    argv = []
 
39
    parser = None           # option parser object
 
40
    options = None          # parsed options
 
41
    args = None             # parsed arguments
 
42
 
 
43
    # real work
 
44
    switcher = None         # switcher object
 
45
    plugins = None          # plugins object
 
46
    log = None
 
47
 
 
48
    def __init__(self):
 
49
        self.log = logging.getLogger('disper')
 
50
        self.log.setLevel(logging.WARNING)
 
51
        self._options_init()
 
52
        self.plugins = Plugins(self)
 
53
        #self.plugins.call('init') # can't really do here since list of plugins isn't read yet
 
54
        self.switcher = Switcher()
 
55
        # add default options
 
56
        # TODO do initial parsing too so errors can be traced to config
 
57
        conffile = os.path.join(os.getenv('HOME'), '.disper', 'config') 
 
58
        if os.path.exists(conffile):
 
59
            f = open(conffile, 'r')
 
60
            opts = ''
 
61
            for l in f.readlines():
 
62
                opts += l.split('#',1)[0] + ' '
 
63
            f.close()
 
64
            self.options_append(shlex.split(opts))
 
65
 
 
66
    def _options_init(self):
 
67
        '''initialize default command-line options'''
 
68
        usage = "usage: %prog [options] (-l|-s|-c|-e|-p|-i)"
 
69
        version = ' '.join(map(str, [self.name, self.version]))
 
70
        self.parser = optparse.OptionParser(usage=usage, version=version)
 
71
 
 
72
        self.add_option('-v', '--verbose', action='store_const', dest='debug', const=logging.INFO,
 
73
            help='show what\'s happening')
 
74
        self.add_option('-q', '--quiet', action='store_const', dest='debug', const=logging.ERROR,
 
75
            help='be quiet and only show errors')
 
76
        self.add_option('-r', '--resolution', dest='resolution',  
 
77
            help='set resolution, e.g. "800x600", or "auto" to detect the display\'s preferred '+
 
78
                 'resolution, or "max" to use the maximum resolution advertised. For extend it '+
 
79
                 'is possible to enter a single resolution for all displays or a comma-separated '+
 
80
                 'list of resolutions (one for each display). Beware that many displays advertise '+
 
81
                 'resolutions they can not fully show, so "max" is not advised.')
 
82
        self.add_option('-d', '--displays', dest='displays',
 
83
            help='comma-separated list of displays to operate on, or "auto" to detect; '+
 
84
                 'the first is the primary display.')
 
85
        self.add_option('-t', '--direction', dest='direction',
 
86
            choices=['left','right','top','bottom'],
 
87
            help='where to extend displays: "left", "right", "top", or "bottom"')
 
88
        self.add_option('', '--scaling', dest='scaling',
 
89
            choices=['default','native','scaled','centered','aspect-scaled'],
 
90
            help='flat-panel scaling mode: "default", "native", "scaled", "centered", or "aspect-scaled"')
 
91
        self.add_option('', '--plugins', dest='plugins',
 
92
            help='comma-separated list of plugins to enable. Special names: "user" for all user plugins '+
 
93
                 'in ~/.disper/hooks; "all" for all plugins found; "none" for no plugins.')
 
94
        self.add_option('', '--cycle-stages', dest='cycle_stages',
 
95
            help='colon-separated list command-line arguments to cycle through')
 
96
 
 
97
        group = optparse.OptionGroup(self.parser, 'Actions',
 
98
            'Select exactly one of the following actions')
 
99
        self._add_option(group, '-l', '--list', action='append_const', const='list', dest='actions',
 
100
            help='list the attached displays')
 
101
        self._add_option(group, '-s', '--single', action='append_const', const='single', dest='actions',
 
102
            help='only enable the primary display')
 
103
        self._add_option(group, '-S', '--secondary', action='append_const', const='secondary', dest='actions',
 
104
            help='only enable the secondary display')
 
105
        self._add_option(group, '-c', '--clone', action='append_const', const='clone', dest='actions',
 
106
            help='clone displays')
 
107
        self._add_option(group, '-e', '--extend', action='append_const', const='extend', dest='actions',
 
108
            help='extend displays')
 
109
        self._add_option(group, '-p', '--export', action='append_const', const='export', dest='actions',
 
110
            help='export current settings to standard output')
 
111
        self._add_option(group, '-i', '--import', action='append_const', const='import', dest='actions',
 
112
            help='import current settings from standard input')
 
113
        self._add_option(group, '-C', '--cycle', action='append_const', const='cycle', dest='actions',
 
114
            help='cycle through the list of cycle stages')
 
115
        self.parser.add_option_group(group)
 
116
 
 
117
 
 
118
    def add_option(self, *args, **kwargs):
 
119
        '''adds an option to the parser. Implements append_const for Python<2.5 too'''
 
120
        return self._add_option(self.parser, *args, **kwargs)
 
121
 
 
122
    def _add_option(self, obj, *args, **kwargs):
 
123
        '''portable optarg add_option function that implements the append_const
 
124
        action for Python versions below 2.5; has an extra first argument as
 
125
        the object on which add_option should be called.'''
 
126
        if sys.hexversion < 0x020500f0 and 'action' in kwargs and \
 
127
                kwargs['action'] == 'append_const':
 
128
            # after: http://permalink.gmane.org/gmane.comp.python.optik.user/284
 
129
            def append_const_cb(const):
 
130
                def cb(opt, opt_str, value, parser):
 
131
                    if not getattr(parser.values, opt.dest):
 
132
                        setattr(parser.values, opt.dest, list())
 
133
                    getattr(parser.values, opt.dest).append(const)
 
134
                return cb
 
135
            kwargs['action'] = 'callback'
 
136
            kwargs['callback'] = append_const_cb(kwargs['const'])
 
137
            del kwargs['const']
 
138
        return obj.add_option(*args, **kwargs)
 
139
 
 
140
 
 
141
    def options_append(self, args):
 
142
        '''parses command-line options; can be called multiple times'''
 
143
        self.argv += args
 
144
 
 
145
    def options_parse(self, args=None):
 
146
        '''parses command-line options given; adds options to current list if set'''
 
147
        if args: self.options_append(args)
 
148
        (self.options, self.args) = self.parser.parse_args(self.argv)
 
149
        # need exactly one action
 
150
        if not self.options.actions: self.options.actions = []
 
151
        elif len(self.options.actions) > 1:
 
152
            self.parser.error('conflicting actions, please specify exactly one action: '
 
153
                         +', '.join(self.options.actions))
 
154
            raise SystemExit(2)
 
155
 
 
156
        if 'import' in self.options.actions or 'export' in self.options.actions:
 
157
            if self.options.resolution:
 
158
                self.log.warning('specified resolution ignored for %s'%self.options.actions[0])
 
159
            if self.options.displays:
 
160
                self.log.warning('specified displays ignored for %s'%self.options.actions[0])
 
161
 
 
162
        # apply defaults here to be able to detect if they were set explicitly or not
 
163
        if not self.options.direction: self.options.direction = "right"
 
164
        if not self.options.resolution: self.options.resolution = "auto"
 
165
        if not self.options.displays: self.options.displays = "auto"
 
166
        if not self.options.scaling: self.options.scaling = "default"
 
167
        if not self.options.debug: self.options.debug = logging.WARNING
 
168
        if self.options.plugins == None: self.options.plugins = "user"
 
169
        self.log.setLevel(self.options.debug)
 
170
        self.options.plugins = map(lambda x: x.strip(), self.options.plugins.split(','))
 
171
        if self.options.displays != 'auto':
 
172
            self.options.displays = map(lambda x: x.strip(), self.options.displays.split(','))
 
173
        if self.options.resolution not in ['auto', 'max']:
 
174
            self.options.resolution = map(lambda x: x.strip(), self.options.resolution.split(','))
 
175
        self.plugins.set_enabled(self.options.plugins)
 
176
 
 
177
    def switch(self):
 
178
        '''Switch to configuration as specified in the options'''
 
179
        if len(self.options.actions) == 0:
 
180
            self.log.info('no action specified')
 
181
            # show help if no action specified
 
182
            self.parser.print_help()
 
183
            raise SystemExit(2)
 
184
        if 'single' in self.options.actions:
 
185
            if self.options.displays != 'auto':
 
186
                self.log.warning('specified displays ignored for single')
 
187
            self.switch_primary()
 
188
        elif 'secondary' in self.options.actions:
 
189
            if self.options.displays != 'auto':
 
190
                self.log.warning('specified displays ignored for secondary')
 
191
            self.switch_secondary()
 
192
        elif 'clone' in self.options.actions:
 
193
            self.switch_clone()
 
194
        elif 'extend' in self.options.actions:
 
195
            self.switch_extend()
 
196
        elif 'export' in self.options.actions:
 
197
            print self.export_config()
 
198
        elif 'import' in self.options.actions:
 
199
            self.import_config('\n'.join(sys.stdin))
 
200
        elif 'cycle' in self.options.actions:
 
201
            self._cycle(self.options.cycle_stages.split(':'))
 
202
        elif 'list' in self.options.actions:
 
203
            # list displays with resolutions
 
204
            displays = self.options.displays
 
205
            if displays == 'auto':
 
206
                displays = self.switcher.get_displays()
 
207
            for disp in displays:
 
208
                res = self.switcher.get_resolutions_display(disp)
 
209
                res.sort()
 
210
                print 'display %s: %s'%(disp, self.switcher.get_display_name(disp))
 
211
                print ' resolutions: '+str(res)
 
212
        else:
 
213
            self.log.critical('program error, unrecognised action: '+', '.join(self.options.actions))
 
214
            raise SystemExit(2)
 
215
 
 
216
    def switch_primary(self, res=None):
 
217
        '''Only enable primary display.
 
218
           @param res resolution to use; or 'auto' for default or None for option'''
 
219
        return self.switch_single(self.switcher.get_primary_display())
 
220
 
 
221
    def switch_secondary(self, res=None):
 
222
        '''Only enable secondary display.
 
223
           @param res resolution to use; or 'auto' for default or None for option'''
 
224
        primary = self.switcher.get_primary_display()
 
225
        try:
 
226
            display = [x for x in self.switcher.get_displays() if x != primary][0]
 
227
        except IndexError:
 
228
            self.log.critical('No secondary display found, falling back to primary.')
 
229
            return self.switch_single(primary, res)
 
230
        return self.switch_single(display, res)
 
231
 
 
232
    def switch_single(self, display=None, res=None):
 
233
        '''Only enable one display.
 
234
           @param display display to enable; or 'auto' for primary or None for option
 
235
           @param res resolution to use; or 'auto' for default or None for option'''
 
236
        if not display: display = self.options.displays
 
237
        if display == 'auto':
 
238
            display = self.switcher.get_primary_display()
 
239
        elif isinstance(display, list) and len(display)>1:
 
240
            self.log.warning('single output requested but multiple specified; using first one')
 
241
            display = display[0]
 
242
        if display: display = [display]
 
243
        return self.switch_clone(display, res)
 
244
 
 
245
    def switch_clone(self, displays=None, res=None):
 
246
        '''Clone displays.
 
247
           @param displays list of displays; or 'auto' for default or None for option
 
248
           @param res resolution; or 'auto' for default, 'max' for max or None for option'''
 
249
        # figure out displays
 
250
        if not displays: displays = self.options.displays
 
251
        if displays == 'auto':
 
252
            displays = self.switcher.get_displays()
 
253
            self.log.info('auto-detected displays: '+', '.join(displays))
 
254
        else:
 
255
            self.log.info('using specified displays: '+', '.join(displays))
 
256
        # figure out resolutions
 
257
        if not res:
 
258
            res = self.options.resolution
 
259
            if type(res)==list or type(res)==tuple:
 
260
                if len(res) != 1: raise TypeError('need single resolution for clone')
 
261
                res = res[0]
 
262
        if res == 'auto' or res == 'max':
 
263
            r = self.switcher.get_resolutions(displays).common()
 
264
            if len(r)==0:
 
265
                self.log.critical('displays share no common resolution')
 
266
                raise SystemExit(1)
 
267
            if res == 'max': # ignore any preferred resolution
 
268
                for s in r: s.weight = 0
 
269
            res = sorted(r)[-1]
 
270
        else:
 
271
            res = Resolution(res)
 
272
        # and switch
 
273
        result = self.switcher.switch_clone(displays, res)
 
274
        self.plugins.set_layout_clone(displays, res)
 
275
        self.plugins.call('switch')
 
276
        return result
 
277
 
 
278
    def switch_extend(self, displays=None, direction=None, ress=None):
 
279
        '''Extend displays.
 
280
           @param displays list of displays; or 'auto for default or None for option
 
281
           @param direction direction to extend; or None for option
 
282
           @param ress list of resolutions; or 'auto' for default or 'max' for max or None for option'''
 
283
        # figure out displays
 
284
        if not displays: displays = self.options.displays
 
285
        if displays == 'auto':
 
286
            displays = self.switcher.get_displays()
 
287
            self.log.info('auto-detected displays: '+', '.join(displays))
 
288
        else:
 
289
            self.log.info('using specified displays: '+', '.join(displays))
 
290
        # figure out resolutions
 
291
        if not ress: ress = self.options.resolution
 
292
        if ress == 'max':     # max resolution for each
 
293
            # override auto-detection weights and get highest resolution
 
294
            ress = self.switcher.get_resolutions(displays)
 
295
            for rl in ress.values():
 
296
                for r in rl: r.weight = 0
 
297
            ress = ress.select()
 
298
            self.log.info('maximum resolutions for displays: '+str(ress))
 
299
        elif ress == 'auto':  # use preferred resolution for each
 
300
            ress = self.switcher.get_resolutions(displays).select()
 
301
            self.log.info('preferred resolutions for displays: '+str(ress))
 
302
        else:                       # list of resolutions specified
 
303
            ress = ResolutionSelection(ress, displays)
 
304
            if len(ress)==1:
 
305
                ress = ress * len(displays)
 
306
            elif len(ress) != len(displays):
 
307
                self.log.critical('resolution: must specify either "auto", "max", a single value, or one for each display')
 
308
                raise SystemExit(2)
 
309
            self.log.info('selected resolutions for displays: '+str(ress))
 
310
        # figure out direction
 
311
        if not direction: direction = self.options.direction
 
312
        # and switch
 
313
        result = self.switcher.switch_extend(displays, direction, ress)
 
314
        self.plugins.set_layout_extend(displays, direction, ress)
 
315
        self.plugins.call('switch')
 
316
        return result
 
317
 
 
318
    def export_config(self):
 
319
        return self.switcher.export_config()
 
320
 
 
321
    def import_config(self, data):
 
322
        result = self.switcher.import_config(data)
 
323
        self.plugins.call('switch')
 
324
        return result
 
325
 
 
326
    def _cycle(self, stages):
 
327
        # read last state
 
328
        stage = 0
 
329
        disperconf = os.path.join(os.getenv('HOME'), '.disper')
 
330
        statefile = os.path.join(disperconf, 'last_cycle_stage')
 
331
        if os.path.exists(statefile):
 
332
            f = open(statefile, 'r')
 
333
            stage = int(f.readline())
 
334
            f.close()
 
335
        # apply next
 
336
        stage += 1
 
337
        if stage >= len(stages): stage = 0
 
338
        self.argv = filter(lambda x: x!='-C' and x!='--cycle', self.argv)
 
339
        self.options_parse(shlex.split(stages[stage]))
 
340
        try:
 
341
            self.switch()
 
342
        finally:
 
343
            # write new state to file; do it here to make sure that a
 
344
            # failing configuration doesn't block the cycling
 
345
            if not os.path.exists(disperconf): os.mkdir(disperconf)
 
346
            f = open(statefile, 'w')
 
347
            f.write(str(stage)+'\n')
 
348
            f.close()
 
349
 
 
350
 
 
351
def main():
 
352
    disper = Disper()
 
353
    disper.options_parse(sys.argv[1:])
 
354
    disper.switch()
 
355
 
 
356
if __name__ == "__main__":
 
357
    # Python 2.3 doesn't support arguments to basicConfig()
 
358
    try: logging.basicConfig(format='%(message)s')
 
359
    except: logging.basicConfig()
 
360
    main()
 
361
 
 
362
# vim:ts=4:sw=4:expandtab: