1
2 '''
3 Defines the main function which parses command line arguments and acts on them.
4 Creates an instance of the L{UIRegistrar} to add a new user interface element
5 (UIE) to the repository on disk when commands for the L{UIRegistrar} are
6 present. If the kill switch is found, kills the running instance of LSR started
7 by this user. Otherwise, starts an instance of the L{AccessEngine} and runs the
8 screen reader.
9
10 The L{main} function in this module parses the command line parameters and
11 dispatches command line options to the other top level functions defined here.
12 The other top level functions split up the work of installing/uninstalling new
13 UIEs (L{install}, L{uninstall}), associating/unassociating (L{associate},
14 L{disassociate}), creating/removing/duplicating profiles (L{createProfile},
15 L{removeProfile}, L{duplicateProfile}), initializing the global list of
16 installed extensions L{initGlobal}, and running the screen reader (L{run}).
17 Other top level functions are provided for convenience such as L{configA11y}
18 and L{generate}.
19
20 See the LSR man pages for help with using command line parameters.
21
22 @author: Peter Parente
23 @organization: IBM Corporation
24 @copyright: Copyright (c) 2005, 2007 IBM Corporation
25 @license: The BSD License
26
27 All rights reserved. This program and the accompanying materials are made
28 available under the terms of the BSD license which accompanies
29 this distribution, and is available at
30 U{http://www.opensource.org/licenses/bsd-license.php}
31 '''
32 try:
33
34 import psyco
35 psyco.profile()
36 except ImportError:
37 pass
38
39 import optparse, os, signal, sys, subprocess, glob, re
40 import UIRegistrar, AEConstants, AEOutput, AELog
41 from i18n import _
42 from SettingsManager import SettingsManager
43 from AEConstants import HOME_USER, HOME_DIR, PROG_VERSION, PROG_DATE, \
44 TEMPLATE_DIR, DEFAULT_ASSOCIATIONS, DEFAULT_PROFILE
45
46
47 wrap_rx=re.compile(u"([\u2e80-\uffff])", re.UNICODE)
48
49 -def wrap(text, width=79, encoding='utf-8', indent=4):
50 '''
51 A word-wrap function that preserves existing line breaks and most spaces in
52 the text. Expects that existing line breaks are posix newlines. Recipe by
53 Junyong Pan from the Python Cookbook:
54
55 U{http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/358117}
56
57 @param text: Text to wrap
58 @type text: string
59 @param width: Maximum width of a line
60 @type width: integer
61 @return: Wrapped text as a string
62 @rtype: string
63 '''
64 return reduce(lambda line, word, width=width: '%s%s%s' %
65 (line,
66 [' ','\n'+' '*indent, ''][(len(line)-line.rfind('\n')-1
67 + len(word.split('\n',1)[0] ) >= width) or
68 line[-1:] == '\0' and 2],
69 word),
70 wrap_rx.sub(r'\1\0 ', unicode(text,encoding)).split(' ')
71 ).replace('\0', '').encode(encoding)
72
81
83 '''
84 Confirms deletion actions from the command prompt. Looks for a press of 'y'
85 to indicate confirmation.
86
87 @param name: Name of the "thing" being removed
88 @type name: string
89 @return: Confirm (True) or cancel (False)?
90 @rtype: boolean
91 '''
92 ch = raw_input(AEConstants.MSG_CONFIRM_REMOVE % name)
93 return ch == 'y'
94
96 '''
97 Installs a new UIE in the repository via the L{UIRegistrar}.
98
99 @param options: Options seen on the command line
100 @type options: optparse.Values
101 @return: Description of the action
102 @rtype: string
103 @raise Exception: When the L{UIRegistrar} reports an error completing the
104 install
105 '''
106 reg = UIRegistrar
107 reg.install(options.action_value, local=not options.uie_global)
108 return _('%s installed' % options.action_value)
109
111 '''
112 Uninstalls a UIE from the repository via the L{UIRegistrar}.
113
114 @param options: Options seen on the command line
115 @type options: optparse.Values
116 @return: Description of the action
117 @rtype: string
118 @raise Exception: When the L{UIRegistrar} reports an error completing the
119 uninstall
120 '''
121
122 if not confirmRemove(options.action_value):
123 return _('Cancelled uninstall of %s') % options.action_value
124 reg = UIRegistrar
125 reg.uninstall(options.action_value, local=not options.uie_global)
126 return _('%s uninstalled' % options.action_value)
127
129 '''
130 Associates a UIE with a profile so that it is loaded at a particular time
131 (e.g. on startup, when any tier is created, or when a particular tier is
132 created).
133
134 @param options: Options seen on the command line
135 @type options: optparse.Values
136 @return: Description of the action
137 @rtype: string
138 @raise Exception: When the L{UIRegistrar} reports an error completing the
139 association
140 '''
141 reg = UIRegistrar
142 profiles = reg.associate(options.action_value, options.profile,
143 options.uie_tier, options.uie_all_tiers,
144 options.uie_index)
145 if not profiles:
146 return _('%s added to no profiles') % options.action_value
147 else:
148 return _('%s added to %s') % (options.action_value, ', '.join(profiles))
149
151 '''
152 Disassociates a UIE with a profile so that it is no longer loaded at a
153 particular time.
154
155 @param options: Options seen on the command line
156 @type options: optparse.Values
157 @return: Description of the action
158 @rtype: string
159 @raise Exception: When the L{UIRegistrar} reports an error completing the
160 disassociation
161 '''
162
163 if not confirmRemove(options.action_value):
164 return _('Cancelled removal of %s') % options.action_value
165 reg = UIRegistrar
166 profiles = reg.disassociate(options.action_value, options.profile,
167 options.uie_tier, options.uie_all_tiers)
168 return _('%s removed from %s' % (options.action_value, ', '.join(profiles)))
169
171 '''
172 Generates a template file for the given kind of UIE.
173
174 @param options: Options seen on the command line
175 @type options: optparse.Values
176 @return: Description of the action
177 @rtype: string
178 @raise Exception: When there is a problem generating the new template because
179 not all required parameters were specified, the template does not exist,
180 the destination path is not writable, etc.
181 '''
182
183 path, kind = options.action_value
184
185 name, ext = os.path.basename(path).split(os.extsep)
186 if options.uie_tier is not None:
187 tier = "'%s'" % options.uie_tier
188 else:
189 tier = None
190
191 try:
192 temp = file(os.path.join(TEMPLATE_DIR, kind), 'r').read()
193 except IOError:
194 raise ValueError(_('No template for %s') % kind)
195 if options.uie_all_tiers:
196 desc = 'all applications'
197 elif tier:
198 desc = options.uie_tier
199 else:
200 desc = 'nothing'
201
202 uie = temp % dict(name=name, tier=tier, all_tiers=options.uie_all_tiers,
203 desc=desc)
204 try:
205 file(path, 'w').write(uie)
206 except IOError:
207 raise ValueError(_('Could not create %s file') % kind)
208
209 options.action_value = path
210 rv = install(options)
211
212 options.action_value = name
213 options.profile = options.profile or AEConstants.GENERATE_PROFILES
214 return associate(options)
215
217 '''
218 Creates a new, empty profile.
219
220 @param options: Options seen on the command line
221 @type options: optparse.Values
222 @return: Description of the action
223 @rtype: string
224 @raise Exception: When the L{SettingsManager} reports an error creating the
225 profile
226 '''
227 sm = SettingsManager(options.action_value)
228 sm.createProfile()
229 return _('Created profile %s') % options.action_value
230
232 '''
233 Duplicates an existing profile.
234
235 @param options: Options seen on the command line
236 @type options: optparse.Values
237 @return: Description of the action
238 @rtype: string
239 @raise Exception: When the L{SettingsManager} reports an error copying the
240 profile
241 '''
242 sm = SettingsManager(options.action_value[0])
243 sm.copyProfile(options.action_value[1])
244 return _('Duplicated profile %s as %s') % options.action_value
245
247 '''
248 Removes an existing profile.
249
250 @param options: Options seen on the command line
251 @type options: optparse.Values
252 @return: Description of the action
253 @rtype: string
254 @raise Exception: When the L{SettingsManager} reports an error deleting the
255 profile
256 '''
257 sm = SettingsManager(options.action_value)
258 sm.ensureProfile()
259
260 if not confirmRemove(options.action_value):
261 return _('Cancelled removal of profile %s') % options.action_value
262 sm.deleteProfile()
263 return _('Removed profile %s') % options.action_value
264
266 '''
267 Gets the path to the package containing this file. Essentially, this is
268 the path to the LSR package on disk.
269
270 @param options: Options seen on the command line
271 @type options: optparse.Values
272 @return: Description of the action
273 @rtype: string
274 '''
275 return os.path.abspath(os.path.normpath(os.path.dirname(__file__)))
276
278 '''
279 Uses the first available audio L{AEOutput} device to output a string.
280
281 @param options: Options seen on the command line
282 @type options: optparse.Values
283 @return: Description of the action
284 @rtype: string
285 '''
286 import DeviceManager, SettingsManager
287
288 if options.profile is None:
289 profile = DEFAULT_PROFILE
290 else:
291 profile = options.profile[0]
292
293 class DummyEngine(object):
294
295 def getProfile(self):
296 return profile
297
298
299 dm = DeviceManager.DeviceManager(DummyEngine())
300 dm.settings_manager = SettingsManager.SettingsManager(profile)
301 dm.loadDevices()
302
303
304 try:
305 dev = dm.getOutputByCaps(['audio'])
306 except Exception:
307 return _('No audio output device')
308
309 s = unicode(options.action_value, 'utf-8')
310 dm.send(dev, AEConstants.CMD_STRING_SYNC, s, None, None)
311 dm.close()
312 return _('Said: %s') % s
313
315 '''
316 Shows all installed scripts and profiles, indicating all associated UIE's
317 via the L{UIRegistrar}.
318
319 @param options: Options seen on the command line
320 @type options: optparse.Values
321 @return: Description of the action
322 @rtype: string
323 '''
324 reg = UIRegistrar
325 if options.profile is None:
326
327 print _('Profiles')
328 print SettingsManager.listProfiles()
329 print
330
331
332 print _('Installed UIEs')
333 i_count = 0
334 for kind in UIRegistrar.ALL_KINDS:
335 names = reg.listInstalled(kind)
336 i_count += len(names)
337 print kind.title()+'s'
338 print wrap(' '+', '.join(names))
339 return _('%d installed UIEs') % i_count
340 else:
341
342 for name in options.profile:
343 print _('Associated UIEs (%s)') % name
344 a_count = 0
345 for kind in UIRegistrar.ALL_KINDS:
346 if kind in UIRegistrar.STARTUP_KINDS:
347 names = reg.listAssociated(kind, name)
348 if not names:
349
350 continue
351 a_count += len(names)
352 print kind.title()+'s'
353 print wrap(' '+', '.join(names))
354 elif kind == UIRegistrar.PERK:
355
356 print kind.title()
357 names = reg.listAssociatedAny(kind, name)
358 a_count += len(names)
359 print wrap(' '+', '.join(names))
360 for app, names in reg.listAssociatedMap(kind, name).items():
361
362 a_count += len(names)
363 print ' (%s)' % app
364 print wrap(' '+', '.join(names))
365 print _('%d associated UIEs') % a_count
366 return ''
367
369 '''
370 Runs the screen reader.
371
372 @param options: Options seen on the command line
373 @type options: optparse.Values
374 @return: Description of the action
375 @rtype: string
376 @raise Exception: When anything goes wrong during initialization
377 '''
378 import Adapters
379
380 if options.profile is None:
381 profile = DEFAULT_PROFILE
382 else:
383 profile = options.profile[0]
384 if profile not in SettingsManager.listProfiles():
385 raise ValueError(_('Profile %s does not exist') % options.profile)
386
387 import AccessEngine
388
389 ae = AccessEngine.AccessEngine(profile, not options.no_intro)
390 rv = ae.run()
391 return rv
392
394 '''
395 Initializes the default user profiles using the L{UIRegistrar}. All UIEs
396 shipped with LSR are associated with their appropriate profiles by this
397 method in a well-defined order specified here.
398
399 @param options: Options seen on the command line
400 @type options: optparse.Values
401 @return: Description of the action
402 @rtype: string
403 @raise Exception: When anything goes wrong during the initialization of the
404 default profiles
405 '''
406 for profile, uies in DEFAULT_ASSOCIATIONS.items():
407 for uie in uies:
408 try:
409 UIRegistrar.associate(uie, profiles=[profile])
410 except (AttributeError, KeyError), e:
411
412 pass
413 return _('Initialized default profiles')
414
416 '''
417 Initializes the global cache of installed UIEs in the L{HOME_DIR} folder
418 using the L{UIRegistrar}. All UIEs in the Perks, Devices, Choosers, and
419 Monitors are installed by this method. Any UIEs with names that do not
420 conflict with the defaults are left untouched by this process.
421
422 @param options: Options seen on the command line
423 @type options: optparse.Values
424 @return: Description of the action
425 @rtype: string
426 @raise ValueError: When anything goes wrong during initialization of the
427 global repository
428 '''
429 uies = []
430 for kind in UIRegistrar.ALL_KINDS:
431 if kind == UIRegistrar.PERK:
432
433 path = os.path.join(HOME_DIR, kind.title()+'s')
434 for subdir in os.listdir(path):
435 subdir = os.path.join(HOME_DIR, kind.title()+'s', subdir)
436 if os.path.isdir(subdir):
437 path = os.path.join(subdir, '*.py')
438 uies.extend(glob.glob(path))
439 else:
440
441 path = os.path.join(HOME_DIR, kind.title()+'s', '*.py')
442 uies.extend(glob.glob(path))
443 for uie in uies:
444 try:
445 UIRegistrar.install(uie, local=False, overwrite=True)
446 except AttributeError:
447
448 pass
449 return _('Initialized global extensions')
450
451 -def getAction(option, opt_str, value, parser):
452 '''
453 Maps a command line param to the name of a function in this module that will
454 handle it. For instance, the command line parameter for installing a UIE is
455 mapped to the L{install} function which will carry out the command.
456 '''
457 parser.values.action = option.default
458 parser.values.action_value = value
459
461 '''
462 Runs gconf to check if desktop accessibility support is enabled or not. If
463 not, enables it and asks the user to logout.
464
465 @note: Current implementation is GNOME, AT-SPI dependent
466 @param options: Options seen on the command line
467 @type options: optparse.Values
468 @raise ValueError: When accessibility support is not enabled
469 '''
470 if options.no_a11y:
471 return
472 try:
473 import gconf
474 except ImportError:
475 return
476 cl = gconf.client_get_default()
477 if not cl.get_bool('/desktop/gnome/interface/accessibility'):
478
479 chooser = UIRegistrar.loadOne('A11yChooser')
480 chooser.init()
481
483 '''
484 Configures the GUI toolkit with a default icon list for all LSR top level
485 windows. Also enables accessibility for this application.
486
487 @note: Current implementation is gtk/gail dependent
488 '''
489 if options.no_bridge:
490
491 os.environ['GTK_MODULES'] = ''
492 else:
493
494 os.environ['GTK_MODULES'] = 'gail:atk-bridge'
495
496 import pygtk
497 pygtk.require('2.0')
498 import gtk
499
500 it = gtk.IconTheme()
501 try:
502 icons = [it.load_icon(AEConstants.PROG_NAME, size, gtk.ICON_LOOKUP_NO_SVG)
503 for size in AEConstants.ICON_SIZES]
504 except Exception:
505
506 pass
507 else:
508 gtk.window_set_default_icon_list(*icons)
509
511 '''
512 Parses the command line arguments based on the definition of the expected
513 arguments provided here.
514
515 @return: Parsed options
516 @rtype: optparse.Values
517 '''
518
519 op = optparse.OptionParser()
520
521 op.add_option('-g', '--generate', action='callback', callback=getAction,
522 help=AEConstants.HELP_GENERATE, default=generate, type='str',
523 nargs=2)
524 op.add_option('-i', '--install', action='callback', callback=getAction,
525 help=AEConstants.HELP_INSTALL, default=install, type='str',
526 nargs=1)
527 op.add_option('-u', '--uninstall', action='callback', callback=getAction,
528 help=AEConstants.HELP_UNINSTALL, default=uninstall,type='str',
529 nargs=1)
530 op.add_option('-a', '--associate', action='callback', callback=getAction,
531 help=AEConstants.HELP_ASSOCIATE, default=associate,type='str',
532 nargs=1)
533 op.add_option('-d', '--disassociate', action='callback', callback=getAction,
534 help=AEConstants.HELP_DISASSOCIATE, default=disassociate,
535 type='str', nargs=1)
536 op.add_option('-c', '--create-profile', action='callback',callback=getAction,
537 help=AEConstants.HELP_CREATE_PROFILE, default=createProfile,
538 type='str', nargs=1)
539 op.add_option('-r', '--remove-profile', action='callback',callback=getAction,
540 help=AEConstants.HELP_REMOVE_PROFILE, default=removeProfile,
541 type='str', nargs=1)
542 op.add_option('--dupe', '--duplicate-profile', action='callback',
543 callback=getAction, default=duplicateProfile,
544 help=AEConstants.HELP_DUPLICATE_PROFILE, type='str', nargs=2)
545 op.add_option('-s', '--show', action='callback', callback=getAction,
546 default=show, help=AEConstants.HELP_SHOW)
547 op.add_option('-p', '--profile', action='append', dest='profile',
548 help=AEConstants.HELP_PROFILE)
549 op.add_option('-y', '--say', action='callback', callback=getAction,
550 help=AEConstants.HELP_SAY, default=say, type='str')
551 op.add_option('--index', dest='uie_index', action='store', type='int',
552 help=AEConstants.HELP_INDEX)
553 op.add_option('--global', action='store_true', default=False,
554 dest='uie_global', help=AEConstants.HELP_GLOBAL)
555 op.add_option('--all-tiers', '--all-apps', action='store_true',
556 default=False,
557 dest='uie_all_tiers', help=AEConstants.HELP_ALL_TIERS)
558 op.add_option('--tier', '--app', dest='uie_tier', action='store', type='str',
559 help=AEConstants.HELP_TIER)
560 op.add_option('-l', '--log-level', dest='log_level', action='store',
561 choices=['none', 'debug', 'print', 'info', 'warning', 'error'],
562 help=AEConstants.HELP_LOG_LEVEL % '[debug, print, info, ' \
563 'warning, error, critical]')
564 op.add_option('--log-channel', dest='log_channels', action='append',
565 help=AEConstants.HELP_LOG_CHANNEL % '[Perk, Tier, ...]')
566 op.add_option('--log-file', dest='log_file', action='store', type='str',
567 help=AEConstants.HELP_LOG_FILENAME)
568 op.add_option('--log-conf', dest='log_conf', action='store', type='str',
569 help=AEConstants.HELP_LOG_CONFIG)
570 op.add_option('--init-global', action='callback', callback=getAction,
571 default=initGlobal, help=AEConstants.HELP_INIT_GLOBAL)
572 op.add_option('--init-profiles', action='callback', callback=getAction,
573 default=initProfiles, help=AEConstants.HELP_INIT_PROFILES)
574 op.add_option('--no-intro', action='store_true', default=False,
575 dest='no_intro', help=AEConstants.HELP_NO_INTRO)
576 op.add_option('--no-a11y', action='store_true', default=False,
577 dest='no_a11y', help=AEConstants.HELP_NO_A11Y)
578 op.add_option('--no-bridge', action='store_true', default=False,
579 dest='no_bridge', help=AEConstants.HELP_NO_BRIDGE)
580 op.add_option('--export-path', action='callback', callback=getAction,
581 default=exportPath, help=AEConstants.HELP_EXPORT_PATH)
582 (options, args) = op.parse_args()
583 return options
584
586 '''
587 Print the welcome message, parses command line arguments, and configures the
588 logging system. Dispatches control to one of the top-level functions in this
589 module depending on the command line parameters specified.
590
591 If the LSR L{AccessEngine} should be run (i.e. not configured or tested),
592 checks that a11y support is enabled in gconf. If not, enables it but exits
593 with an error.
594 '''
595
596 options = parseOptions()
597
598 try:
599
600 mtd = options.action
601 except AttributeError:
602
603 mtd = run
604
605
606
607
608 if mtd is not initGlobal and not SettingsManager.hasDefaultProfiles():
609 initProfiles(None)
610
611 if mtd is not exportPath:
612
613 welcome()
614
615 if mtd is run:
616
617 configGUI(options)
618
619 configA11y(options)
620
621 AELog.configure(options.log_conf, options.log_level, options.log_channels,
622 options.log_file)
623
624 mtd(options)
625 else:
626 import traceback as tb
627 try:
628
629 print mtd(options)
630 except Exception, e:
631 info = tb.extract_tb(sys.exc_info()[2])
632 print '%s (%d): %s' % (os.path.basename(info[-1][0]), info[-1][1], e)
633
634 if __name__ == '__main__':
635 main()
636