2
###############################################################################
3
# disper.py - main disper
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.
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.
23
from switcher import Switcher, Resolution, ResolutionSelection
24
from plugins import Plugins
27
# make sure to handle SystemExit when using this class in a 3rd-party program
35
prefix_share = build.prefix_share
39
parser = None # option parser object
40
options = None # parsed options
41
args = None # parsed arguments
44
switcher = None # switcher object
45
plugins = None # plugins object
49
self.log = logging.getLogger('disper')
50
self.log.setLevel(logging.WARNING)
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()
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')
61
for l in f.readlines():
62
opts += l.split('#',1)[0] + ' '
64
self.options_append(shlex.split(opts))
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)
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')
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)
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)
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)
135
kwargs['action'] = 'callback'
136
kwargs['callback'] = append_const_cb(kwargs['const'])
138
return obj.add_option(*args, **kwargs)
141
def options_append(self, args):
142
'''parses command-line options; can be called multiple times'''
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))
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])
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)
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()
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:
194
elif 'extend' in self.options.actions:
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)
210
print 'display %s: %s'%(disp, self.switcher.get_display_name(disp))
211
print ' resolutions: '+str(res)
213
self.log.critical('program error, unrecognised action: '+', '.join(self.options.actions))
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())
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()
226
display = [x for x in self.switcher.get_displays() if x != primary][0]
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)
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')
242
if display: display = [display]
243
return self.switch_clone(display, res)
245
def switch_clone(self, displays=None, res=None):
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))
255
self.log.info('using specified displays: '+', '.join(displays))
256
# figure out resolutions
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')
262
if res == 'auto' or res == 'max':
263
r = self.switcher.get_resolutions(displays).common()
265
self.log.critical('displays share no common resolution')
267
if res == 'max': # ignore any preferred resolution
268
for s in r: s.weight = 0
271
res = Resolution(res)
273
result = self.switcher.switch_clone(displays, res)
274
self.plugins.set_layout_clone(displays, res)
275
self.plugins.call('switch')
278
def switch_extend(self, displays=None, direction=None, ress=None):
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))
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
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)
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')
309
self.log.info('selected resolutions for displays: '+str(ress))
310
# figure out direction
311
if not direction: direction = self.options.direction
313
result = self.switcher.switch_extend(displays, direction, ress)
314
self.plugins.set_layout_extend(displays, direction, ress)
315
self.plugins.call('switch')
318
def export_config(self):
319
return self.switcher.export_config()
321
def import_config(self, data):
322
result = self.switcher.import_config(data)
323
self.plugins.call('switch')
326
def _cycle(self, stages):
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())
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]))
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')
353
disper.options_parse(sys.argv[1:])
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()
362
# vim:ts=4:sw=4:expandtab: