Let application decides on how to handle signals.
[laditools.git] / ladi-control-center
blobaece6bff2edda336c2e5772f42af1dd003020a9f
1 #!/usr/bin/python
3 # LADITools - Linux Audio Desktop Integration Tools
4 # ladi-control-center - A configuration GUI for your Linux Audio Desktop
5 # Copyright (C) 2011-2012 Alessio Treglia <quadrispro@ubuntu.com>
6 # Copyright (C) 2007-2010, Marc-Olivier Barre <marco@marcochapeau.org>
7 # Copyright (C) 2007-2009, Nedko Arnaudov <nedko@arnaudov.name>
8 # Copyright (C) 2008, Krzysztof Foltman <wdev@foltman.com>
10 # This program is free software: you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation, either version 3 of the License, or
13 # (at your option) any later version.
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with this program. If not, see <http://www.gnu.org/licenses/>.
23 import os
24 import sys
25 import signal
26 import gettext
27 import argparse
29 from laditools import _gettext_domain
30 gettext.install(_gettext_domain)
32 from laditools import get_version_string
33 from laditools import check_ladish
34 from laditools import LadishProxy
35 from laditools import LadishStatusType
36 from laditools import LadishProxyError
37 from laditools import JackConfigProxy
39 from gi.repository import Gtk
40 from gi.repository import Gdk
41 from gi.repository import GObject
43 from laditools.gtk import find_data_file
45 sig_handler = signal.getsignal(signal.SIGTERM)
46 signal.signal(signal.SIGINT, sig_handler)
48 tooltips_active = True
50 (COLUMN_NAME,
51 COLUMN_PARAMETER,
52 COLUMN_SHORTDESC,
53 COLUMN_LONGDESC,
54 COLUMN_ISSET) = range (5)
56 def _check_ladish():
57 try:
58 ret = check_ladish()
59 except LadishProxyError as e:
60 sys.stderr.write("%s\n" % str(e))
61 sys.stderr.flush()
62 sys.exit(1)
63 except Exception as e:
64 sys.stderr.write(_("ladish proxy creation failed: %s\n") % str(e))
65 sys.stderr.flush()
66 sys.exit(1)
67 if ret == LadishStatusType.STUDIO_STOPPED:
68 # Everything is OK, there's no need to print out a message
69 #sys.stderr.write(_("ladish studio is loaded and not started\n"))
70 #sys.stderr.flush()
71 return False
72 elif ret == LadishStatusType.NOT_AVAILABLE:
73 sys.stderr.write(_("ladish is not available\n"))
74 sys.stderr.flush()
75 sys.exit(1)
76 else:
77 if ret == LadishStatusType.NO_STUDIO_LOADED:
78 msg = _("JACK can only be configured with a loaded and stopped studio. Please create a new studio or load and stop an existing one.")
79 sys.stderr.write(msg + "\n")
80 title = _("No studio present")
81 elif ret == LadishStatusType.STUDIO_RUNNING:
82 msg = _("JACK can only be configured with a stopped studio. Please stop your studio first.")
83 sys.stderr.write(msg + "\n")
84 title = _("Studio is running")
85 else:
86 sys.stderr(_("Unexpected error!\n"))
87 try:
88 mdlg = Gtk.MessageDialog(type = Gtk.MessageType.ERROR,
89 buttons = Gtk.ButtonsType.CLOSE,
90 message_format = msg)
91 mdlg.set_title(title)
92 mdlg.run()
93 mdlg.hide()
94 except:
95 pass
96 finally:
97 sys.stderr.flush()
98 sys.exit(1)
100 class parameter(object):
101 def __init__(self, path):
102 self.path = path
103 self.name = path[-1:]
105 def get_name(self):
106 return self.name
108 def get_type(self):
109 return jack.get_param_type(self.path)
111 def get_value(self):
112 return jack.get_param_value(self.path)
114 def set_value(self, value):
115 jack.set_param_value(self.path, value)
117 def reset_value(self):
118 jack.reset_param_value(self.path)
120 def get_short_description(self):
121 return jack.get_param_short_description(self.path)
123 def get_long_description(self):
124 descr = jack.get_param_long_description(self.path)
125 if not descr:
126 descr = self.get_short_description()
127 return descr
129 def has_range(self):
130 return jack.param_has_range(self.path)
132 def get_range(self):
133 return jack.param_get_range(self.path)
135 def has_enum(self):
136 return jack.param_has_enum(self.path)
138 def is_strict_enum(self):
139 return jack.param_is_strict_enum(self.path)
141 def is_fake_values_enum(self):
142 return jack.param_is_fake_value(self.path)
144 def get_enum_values(self):
145 return jack.param_get_enum_values(self.path)
147 class configure_command(object):
148 def __init__(self):
149 pass
151 def get_description(self):
152 pass
154 def get_window_title(self):
155 return self.get_description();
157 def run(self, args):
158 return self.activate()
160 class parameter_enum_value(GObject.GObject):
161 def __init__(self, is_fake_value, value, description):
162 GObject.GObject.__init__(self)
163 self.is_fake_value = is_fake_value
164 self.value = value
165 self.description = description
167 def get_description(self):
168 if self.is_fake_value:
169 return self.description
171 return str(self.value) + " - " + self.description
173 class parameter_store(GObject.GObject):
174 def __init__(self, param):
175 GObject.GObject.__init__(self)
176 self.param = param
177 self.name = self.param.get_name()
178 self.is_set, self.default_value, self.value = self.param.get_value()
179 self.modified = False
180 self.has_range = self.param.has_range()
181 self.is_strict = self.param.is_strict_enum()
182 self.is_fake_value = self.param.is_fake_values_enum()
184 self.enum_values = []
186 if self.has_range:
187 self.range_min, self.range_max = self.param.get_range()
188 else:
189 for enum_value in self.param.get_enum_values():
190 self.enum_values.append(parameter_enum_value(self.is_fake_value, enum_value[0], enum_value[1]))
192 def get_name(self):
193 return self.name
195 def get_type(self):
196 return self.param.get_type()
198 def get_value(self):
199 return self.value
201 def get_default_value(self):
202 if not self.is_fake_value:
203 return str(self.default_value)
205 for enum_value in self.get_enum_values():
206 if enum_value.value == self.default_value:
207 return enum_value.get_description()
209 return "???"
211 def set_value(self, value):
212 self.value = value
213 self.modified = True
215 def reset_value(self):
216 self.value = self.default_value
218 def get_short_description(self):
219 return self.param.get_short_description()
221 def maybe_save_value(self):
222 if self.modified:
223 self.param.set_value(self.value)
224 self.modified = False
226 def get_range(self):
227 return self.range_min, self.range_max
229 def has_enum(self):
230 return len(self.enum_values) != 0
232 def is_strict_enum(self):
233 return self.is_strict
235 def get_enum_values(self):
236 return self.enum_values
238 GObject.type_register(parameter_store)
240 def combobox_get_active_text(combobox, model_index = 0):
241 model = combobox.get_model()
242 active = combobox.get_active()
243 if active < 0:
244 return None
245 return model[active][model_index]
247 class cell_renderer_param(Gtk.CellRendererPixbuf):
248 __gproperties__ = { "parameter": (GObject.TYPE_OBJECT,
249 "Parameter",
250 "Parameter",
251 GObject.PARAM_READWRITE) }
253 def __init__(self):
254 Gtk.CellRendererPixbuf.__init__(self)
255 self.parameter = None
256 self.set_property('mode', Gtk.CellRendererMode.EDITABLE)
257 self.set_property('mode', Gtk.CellRendererMode.ACTIVATABLE)
258 self.renderer_text = Gtk.CellRendererText()
259 self.renderer_toggle = Gtk.CellRendererToggle()
260 self.renderer_combo = Gtk.CellRendererCombo()
261 self.renderer_spinbutton = Gtk.CellRendererSpin()
262 for r in (self.renderer_text, self.renderer_combo, self.renderer_spinbutton):
263 r.connect("edited", self.on_edited)
264 self.renderer = None
265 self.edit_widget = None
267 def do_set_property(self, pspec, value):
268 if pspec.name == 'parameter':
269 if value.get_type() == 'b':
270 self.set_property('mode', Gtk.CellRendererMode.ACTIVATABLE)
271 else:
272 self.set_property('mode', Gtk.CellRendererMode.EDITABLE)
273 else:
274 sys.stderr.write(pspec.name)
275 setattr(self, pspec.name, value)
277 def do_get_property(self, pspec):
278 return getattr(self, pspec.name)
280 def choose_renderer(self):
281 typechar = self.parameter.get_type()
282 value = self.parameter.get_value()
284 if typechar == "b":
285 self.renderer = self.renderer_toggle
286 self.renderer.set_activatable(True)
287 self.renderer.set_active(value)
288 self.renderer.set_property("xalign", 0.0)
289 return
291 if self.parameter.has_enum():
292 self.renderer = self.renderer_combo
294 m = Gtk.ListStore(str, parameter_enum_value)
296 for value in self.parameter.get_enum_values():
297 m.append([value.get_description(), value])
299 self.renderer.set_property("model",m)
300 self.renderer.set_property('text-column', 0)
301 self.renderer.set_property('editable', True)
302 self.renderer.set_property('has_entry', not self.parameter.is_strict_enum())
304 value = self.parameter.get_value()
305 if self.parameter.is_fake_value:
306 text = "???"
307 for enum_value in self.parameter.get_enum_values():
308 if enum_value.value == value:
309 text = enum_value.get_description()
310 break
311 else:
312 text = str(value)
314 self.renderer.set_property('text', text)
316 return
318 if typechar == 'u' or typechar == 'i':
319 self.renderer = self.renderer_spinbutton
320 self.renderer.set_property('text', str(value))
321 self.renderer.set_property('editable', True)
322 if self.parameter.has_range:
323 range_min, range_max = self.parameter.get_range()
324 self.renderer.set_property('adjustment', Gtk.Adjustment(value, range_min, range_max, 1, abs(int((range_max - range_min) / 10))))
325 else:
326 self.renderer.set_property('adjustment', Gtk.Adjustment(value, 0, 100000, 1, 1000))
327 return
329 self.renderer = self.renderer_text
330 self.renderer.set_property('editable', True)
331 self.renderer.set_property('text', self.parameter.get_value())
333 def do_render(self, ctx, widget, bg_area, cell_area, flags):
334 self.choose_renderer()
335 return self.renderer.render(ctx, widget, bg_area, cell_area, flags)
337 def do_get_size(self, widget, cell_area=None):
338 self.choose_renderer()
339 return self.renderer.get_size(widget, cell_area)
341 def do_activate(self, event, widget, path, background_area, cell_area, flags):
342 self.choose_renderer()
343 if self.parameter.get_type() == 'b':
344 self.parameter.set_value(not self.parameter.get_value())
345 widget.get_model()[path][COLUMN_ISSET] = "modified"
346 return True
348 def on_edited(self, renderer, path, value_str):
349 parameter = self.edit_parameter
350 widget = self.edit_widget
351 model = self.edit_tree.get_model()
352 self.edit_widget = None
353 typechar = parameter.get_type()
354 if type(widget) == Gtk.ComboBox:
355 value = combobox_get_active_text(widget, 1)
356 if value == None:
357 return
358 value_str = value.value
359 elif type(widget) == Gtk.ComboBoxText:
360 enum_value = combobox_get_active_text(widget, 1)
361 if enum_value:
362 value_str = enum_value.value
363 else:
364 value_str = widget.get_active_text()
366 if typechar == 'u' or typechar == 'i':
367 try:
368 value = int(value_str)
369 except ValueError, e:
370 # Hide the widget (because it may display something else than what user typed in)
371 widget.hide()
372 # Display the error
373 mdlg = Gtk.MessageDialog(buttons = Gtk.ButtonsType.OK, message_format = "Invalid value. Please enter an integer number.")
374 mdlg.run()
375 mdlg.hide()
376 # Return the focus back to the tree to prevent buttons from stealing it
377 self.edit_tree.grab_focus()
378 return
379 else:
380 value = value_str
381 parameter.set_value(value)
382 model[path][COLUMN_ISSET] = "modified"
383 self.edit_tree.grab_focus()
385 def do_start_editing(self, event, widget, path, background_area, cell_area, flags):
386 # this happens when edit requested using keyboard
387 if not event:
388 event = Gdk.Event(Gdk.NOTHING)
390 self.choose_renderer()
391 ret = self.renderer.start_editing(event, widget, path, background_area, cell_area, flags)
392 self.edit_widget = ret
393 self.edit_tree = widget
394 self.edit_parameter = self.parameter
395 return ret
397 GObject.type_register(cell_renderer_param)
399 class jack_params_configure_command(configure_command):
400 def __init__(self, path):
401 self.path = path
402 self.is_setting = False
404 def reset_value(self, row_path):
405 row = self.liststore[row_path]
406 param = row[COLUMN_PARAMETER]
407 param.reset_value()
408 row[COLUMN_ISSET] = "reset"
410 def do_row_activated(self, treeview, path, view_column):
411 if view_column == self.tvcolumn_is_set:
412 self.reset_value(path)
414 def do_button_press_event(self, tree, event):
415 if event.type != Gdk.EventType._2BUTTON_PRESS:
416 return False
417 # this is needed for proper double-click handling in the list; don't ask me why, I don't know
418 # it's probably because _2BUTTON_PRESS event is still delivered to tree view, automatically deactivating
419 # the newly created edit widget (which gets created on second BUTTON_PRESS but before _2BUTTON_PRESS)
420 # deactivating the widget causes it to be deleted
421 return True
423 def do_key_press_event(self, tree, event):
424 (row_path, cur) = self.treeview.get_cursor()
426 # if Delete was pressed, reset the value
427 #if event.get_state() == 0 and event.keyval == Gdk.KEY_Delete:
428 if event.keyval == Gdk.KEY_Delete:
429 self.reset_value(row_path)
430 tree.queue_draw()
431 return True
433 # prevent ESC from activating the editor
434 if event.string < " ":
435 return False
437 # single-key data entry: if the control is a text entry, spin button or combo box/combo box entry,
438 # then edit the current and set the text value to what user has already typed in
439 (row_path, cur) = self.treeview.get_cursor()
440 param = self.liststore[row_path][COLUMN_PARAMETER]
441 ptype = param.get_type()
443 # we don't care about booleans
444 if ptype == 'b':
445 return False
447 # accept only digits for integer input (or a minus, but only if it's a signed field)
448 if ptype in ('i', 'u'):
449 if not (event.string.isdigit() or (event.string == "-" and ptype == 'i')):
450 return False
452 # Start cell editing
453 # MAYBE: call a specially crafted cell_renderer_param method for this
454 self.treeview.set_cursor_on_cell(row_path, self.tvcolumn_value, self.renderer_value.renderer, True)
456 # cell_renderer_param::on_start_editing() should set edit_widget.
457 # if edit operation has failed (didn't create a widget), pass the key on
458 # MAYBE: call a specially crafted cell_renderer_param method to do this check
459 if self.renderer_value.edit_widget == None:
460 return
462 widget = self.renderer_value.edit_widget
463 if type(widget) in (Gtk.Entry, Gtk.ComboBoxText):
464 # (combo or plain) text entry - set the content and move the cursor to the end
465 sl = len(event.string)
466 widget.set_text(event.string)
467 widget.select_region(sl, sl)
468 return True
470 if type(self.renderer_value.edit_widget) == Gtk.SpinButton:
471 # spin button - set the value and move the cursor to the end
472 if event.string == "-":
473 # special case for minus sign (which can't be converted to float)
474 widget.set_text(event.string)
475 else:
476 widget.set_value(float(event.string))
477 sl = len(widget.get_text())
478 widget.select_region(sl, sl)
479 return True
481 if type(self.renderer_value.edit_widget) == Gtk.ComboBox:
482 # combo box - select the first item that starts with typed character
483 model = widget.get_model()
484 item = -1
485 iter = model.get_iter_root()
486 while iter != None:
487 if model.get_value(iter, 0).startswith(event.string):
488 item = model.get_path(iter)[0]
489 break
490 iter = model.iter_next(iter)
491 widget.set_active(item)
492 return True
494 return False
496 def on_cursor_changed(self, tree):
497 (row_path, cur) = self.treeview.get_cursor()
499 if not self.is_setting and cur != None and cur.get_title() != self.tvcolumn_value.get_title():
500 self.is_setting = True
501 try:
502 self.treeview.set_cursor_on_cell(row_path, self.tvcolumn_value, self.renderer_value.renderer, False)
503 finally:
504 self.is_setting = False
506 def ok_clicked(self, dlg):
507 if self.renderer_value.edit_widget:
508 self.renderer_value.edit_widget.editing_done()
510 def _on_query_tooltip(self, treeview, x, y, keyboard_tip, tooltip):
511 """Handle tooltips for the cells"""
512 try:
513 (path, column, out_x, out_y) = treeview.get_path_at_pos(x, y)
514 if not path:
515 return False
516 except TypeError:
517 return False
519 # Horrible-fix to skip the treeview's header row
520 intpath = int(path.to_string())
521 if intpath == 0:
522 return False
523 elif intpath > 0:
524 path = Gtk.TreePath(str(intpath - 1))
526 text = ''
527 if column.get_title() in (self.tvcolumn_value.get_title(),
528 self.tvcolumn_parameter.get_title()):
529 text = self.liststore[path][COLUMN_LONGDESC]
530 elif column.get_title() == self.tvcolumn_is_set.get_title():
531 if self.liststore[path][COLUMN_ISSET] == "default":
532 return False
533 if self.liststore[path][COLUMN_ISSET] == 'set':
534 text += _("Double-click to schedule reset of value to %s") % \
535 self.liststore[path][COLUMN_PARAMETER].get_default_value()
536 else:
537 text = _("Value will be reset to %s") % \
538 self.liststore[path][COLUMN_PARAMETER].get_default_value()
539 else:
540 return False
542 tooltip.set_text(text)
543 return True
545 def do_destroy(self, *args):
546 for row in self.liststore:
547 param = row[COLUMN_PARAMETER]
548 reset = (row[COLUMN_ISSET] == "reset")
549 if reset:
550 param.param.reset_value()
551 else:
552 if param.modified:
553 param.maybe_save_value()
555 def activate(self, *args, **kwargs):
557 self.liststore = Gtk.ListStore(GObject.TYPE_STRING,
558 parameter_store,
559 GObject.TYPE_STRING,
560 GObject.TYPE_STRING,
561 GObject.TYPE_STRING)
562 self.treeview = Gtk.TreeView(self.liststore)
563 self.treeview.set_rules_hint(True)
564 self.treeview.set_has_tooltip(True)
565 self.treeview.set_enable_search(False)
567 renderer_text = Gtk.CellRendererText()
568 renderer_toggle = Gtk.CellRendererToggle()
569 renderer_value = cell_renderer_param()
570 self.renderer_value = renderer_value # save for use in event handler methods
572 self.tvcolumn_parameter = Gtk.TreeViewColumn('Parameter', renderer_text, text=COLUMN_NAME)
573 self.tvcolumn_is_set = Gtk.TreeViewColumn('Status', renderer_text, text=COLUMN_ISSET)
574 self.tvcolumn_value = Gtk.TreeViewColumn('Value', renderer_value, parameter=COLUMN_PARAMETER)
575 self.tvcolumn_description = Gtk.TreeViewColumn('Description', renderer_text, text=COLUMN_SHORTDESC)
577 self.tvcolumn_value.set_resizable(True)
578 self.tvcolumn_value.set_min_width(100)
580 self.treeview.append_column(self.tvcolumn_parameter)
581 self.treeview.append_column(self.tvcolumn_is_set)
582 self.treeview.append_column(self.tvcolumn_value)
583 self.treeview.append_column(self.tvcolumn_description)
585 param_names = jack.get_param_names(self.path)
586 for name in param_names:
587 param = parameter(self.path + [name])
588 store = parameter_store(param)
589 if store.is_set:
590 is_set = "set"
591 else:
592 is_set = "default"
593 self.liststore.append([name,
594 store,
595 param.get_short_description(),
596 param.get_long_description(),
597 is_set])
599 self.treeview.connect("row-activated", self.do_row_activated)
600 self.treeview.connect("cursor-changed", self.on_cursor_changed)
601 self.treeview.connect("key-press-event", self.do_key_press_event)
602 self.treeview.connect("button-press-event", self.do_button_press_event)
603 if tooltips_active:
604 self.treeview.connect("query-tooltip", self._on_query_tooltip)
605 self.treeview.connect("destroy", self.do_destroy)
607 if len(param_names):
608 # move cursor to first row and 'value' column
609 self.treeview.set_cursor(Gtk.TreePath(path=0),
610 focus_column=self.tvcolumn_value,
611 start_editing=False)
613 return self.treeview
615 class jack_engine_params_configure_command(jack_params_configure_command):
616 def __init__(self):
617 jack_params_configure_command.__init__(self, ['engine'])
619 def get_description(self):
620 return _('JACK engine')
622 class jack_driver_params_configure_command(jack_params_configure_command):
623 def __init__(self):
624 jack_params_configure_command.__init__(self, ['driver'])
626 def get_description(self):
627 return _('JACK driver')
629 def get_window_title(self):
630 return _('JACK "%s" driver') % jack.get_selected_driver()
632 class jack_internal_params_configure_command(jack_params_configure_command):
633 def __init__(self, name):
634 self.name = name
635 jack_params_configure_command.__init__(self, ['internals', name])
637 def get_description(self):
638 return _('JACK "%s"') % self.name
640 def show_panels(*args, **kwargs):
641 if not 'modules' in kwargs:
642 return None
643 mods = kwargs['modules']
644 window = Gtk.Window.new(Gtk.WindowType.TOPLEVEL)
645 vbox = Gtk.VBox()
646 hbox = Gtk.HBox()
647 notebook = Gtk.Notebook()
649 vbox.pack_start(hbox, True, True, 12)
650 hbox.pack_start(notebook, True, True, 2)
651 window.set_title("LADI Settings")
652 window.set_icon_name('preferences-system')
653 window.set_resizable(True)
654 notebook.set_tab_pos(Gtk.PositionType.LEFT)
656 if 'select' in kwargs:
657 selected = kwargs['select']
658 else:
659 selected = None
661 page_count = 0
663 for mod in mods:
664 treeview = mods[mod].run({})
665 container = Gtk.ScrolledWindow()
666 container.set_min_content_width(400)
667 container.set_min_content_height(400)
668 container.set_policy(hscrollbar_policy=Gtk.PolicyType.AUTOMATIC,
669 vscrollbar_policy=Gtk.PolicyType.AUTOMATIC)
670 container.add(treeview)
671 container.show_all()
672 #vbox.show_all()
673 try:
674 tab_label = mods[mod].get_window_title()
675 except:
676 tab_label = mods[mod].get_description()
677 notebook.append_page(container, Gtk.Label(tab_label))
678 if selected and selected == mod:
679 notebook.set_current_page(page_count)
680 page_count += 1
682 window.add(vbox)
684 window.show_all()
685 window.connect('destroy', Gtk.main_quit)
686 Gtk.main()
688 if __name__ == "__main__":
689 global jack
691 jack = JackConfigProxy()
692 _check_ladish()
694 GObject.type_register(parameter_enum_value)
696 parser = argparse.ArgumentParser(description=_('Convenient graphical interface for configuring JACK'),
697 epilog=_('This program is part of the LADITools suite.'))
698 parser.add_argument('-m', '--module', nargs=1, metavar='MODULE', help=_('select the module to configure'))
699 parser.add_argument('-l', '--list-modules', action='store_true', help=_('list available modules'))
700 parser.add_argument('--version', action='version', version="%(prog)s " + get_version_string())
702 options = parser.parse_args()
704 modules = {'engine' : jack_engine_params_configure_command(),
705 'params' : jack_driver_params_configure_command()}
706 for internal in jack.read_container(['internals']):
707 modules[str(internal)] = jack_internal_params_configure_command(internal)
709 if options.list_modules and options.module:
710 sys.stderr.write(_("Conflicting options, type %s --help for a list of options.") % sys.argv[0] + '\n')
711 sys.stderr.flush()
712 sys.exit(2)
714 if options.list_modules:
715 sys.stderr.write(_("Available modules: "))
716 sys.stderr.write(' '.join(modules) + '\n')
717 sys.stderr.flush()
718 elif options.module:
719 module = options.module[0]
720 if not module in modules:
721 sys.stderr.write(_("Module %(mod)s is not available, type '%(cmd)s -l' for a list of available modules.") % {'mod' : module, 'cmd' : sys.argv[0]} + "\n")
722 sys.stderr.flush()
723 sys.exit(2)
724 else:
725 show_panels(modules=modules, select=module)
726 else:
727 show_panels(modules=modules)
729 sys.exit(0)