diff --git a/.bzrignore b/.bzrignore index a8c77bec..2800e844 100644 --- a/.bzrignore +++ b/.bzrignore @@ -2,3 +2,6 @@ terminatorlib/*.pyc .project .pydevproject +terminatorlib/meliae +_trial_temp +terminatorc diff --git a/INSTALL b/INSTALL index 28857f8c..8776a8f4 100644 --- a/INSTALL +++ b/INSTALL @@ -7,7 +7,7 @@ for many distributions at: If you don't have this option, please make sure you satisfy Terminator's dependencies yourself: - * Python 2.4+, 2.6 recommended: + * Python 2.5+, 2.6 recommended: Debian/Ubuntu: python FreeBSD: lang/python26 @@ -15,12 +15,6 @@ dependencies yourself: Debian/Ubuntu: python-vte FreeBSD: x11-toolkits/py-vte -If you want gnome-terminal profile support, you also need: - - * Python GNOME 2 bindings: - Debian/Ubuntu: python-gnome2 - FreeBSD: x11-toolkits/py-gnome2 - If you don't care about native language support or icons, Terminator should run just fine directly from this directory, just: diff --git a/README b/README index 74c8cd9b..52522fd4 100644 --- a/README +++ b/README @@ -1,4 +1,4 @@ -Terminator 0.14 +Terminator 0.90 by Chris Jones and others. The goal of this project is to produce a useful tool for arranging terminals. diff --git a/TODO b/TODO deleted file mode 100644 index ae46d2fc..00000000 --- a/TODO +++ /dev/null @@ -1,6 +0,0 @@ -* menu entry/keybinding to hightlight a term upon: - * command ending - * new text in window - * when a command exits, "window-title-changed" is emitted - even though the actual title string do not change - * text-modified could be used to spy on outputs from the command diff --git a/debian/changelog b/debian/changelog index 16aea692..9d2112a7 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +terminator (0.90~alpha1) lucid; urgency=low + + * New upstream pre-release + + -- Chris Jones Tue, 05 Jan 2010 09:56:27 +0000 + terminator (0.14) karmic; urgency=low * New upstream release diff --git a/debian/control b/debian/control index e73beadf..96c37fb5 100644 --- a/debian/control +++ b/debian/control @@ -14,7 +14,7 @@ Homepage: http://www.tenshu.net/terminator/ Package: terminator Architecture: all -Depends: ${python:Depends}, ${misc:Depends}, python-vte, python-gobject, python-gtk2, gconf2, libgtk2.0-bin +Depends: ${python:Depends}, ${misc:Depends}, python-vte, python-gobject, python-gtk2 (>= 2.14.0), gconf2, libgtk2.0-bin XB-Python-Version: ${python:Versions} Provides: x-terminal-emulator Recommends: xdg-utils, python-xdg, python-gnome2, deskbar-applet diff --git a/debian/copyright b/debian/copyright index bd2183da..26437c9f 100644 --- a/debian/copyright +++ b/debian/copyright @@ -33,13 +33,14 @@ Translations: "Data" Cristian Grada "zhuqin" + and many others. Seriously, thank you very much to the translators. A few minutes of their dedication opens up userbases like nothing else. Copyright: - + License: diff --git a/doc/terminator.1 b/doc/terminator.1 index 500ca2b9..3873ab82 100644 --- a/doc/terminator.1 +++ b/doc/terminator.1 @@ -41,13 +41,6 @@ with the \fBhide_window\fR keyboard shortcut (Ctrl-Shift-Alt-a by default) Force the Terminator window to use a specific name rather than updating it dynamically based on the wishes of the child shell. .TP -.B \-\-no_gconf -Ignore the gconf settings of gnome-terminal -.TP -.B \-p, \-\-profile PROFILE -Loads the GNOME Terminal profile named PROFILE. Note that doing this will override the settings -in your Terminator config file with those from the GNOME Terminal profile. -.TP .B \-\-geometry=GEOMETRY Specifies the preferred size and position of Terminator's window; see X(7). .TP diff --git a/doc/terminator_config.5 b/doc/terminator_config.5 index a497a043..6759a83f 100644 --- a/doc/terminator_config.5 +++ b/doc/terminator_config.5 @@ -3,7 +3,9 @@ ~/.config/terminator/config \- the config file for Terminator terminal emulator. .SH "DESCRIPTION" This manual page documents briefly the -.B Terminator config file. +.B Terminator +config file. +.B IT IS FULL OF LIES. THE CONFIG FILE FORMAT HAS COMPLETELY CHANGED. .PP \fBterminator/config\fP is an optional file to configure the terminator terminal emulator. It is used to control options not in gnome-terminal gconf profiles, or override gconf settings. .SH "FILE LOCATION" @@ -119,10 +121,6 @@ Default value: \fBTrue\fR If true, a titlebar will be drawn on zoomed/maximised terminals which indicates how many are hidden. Default value: \fBTrue\fR .TP -.B titletips -If true, a tooltip will be available for each terminal which shows the current title of that terminal. -Default value: \fBFalse\fR -.TP .B title_tx_txt_color Sets the colour of the text shown in the titlebar of the active terminal. Default value: \fB#FFFFFF\fR diff --git a/setup.py b/setup.py index 38be0b4e..919cfb04 100755 --- a/setup.py +++ b/setup.py @@ -6,14 +6,15 @@ from distutils.cmd import Command from distutils.command.install_data import install_data from distutils.command.build import build from distutils.dep_util import newer -from distutils.log import warn, info, error, fatal +from distutils.log import warn, info, error +from distutils.errors import DistutilsFileError import glob import os import sys import subprocess import platform -from terminatorlib.version import * +from terminatorlib.version import APP_NAME, APP_VERSION PO_DIR = 'po' MO_DIR = os.path.join('build', 'mo') @@ -83,6 +84,8 @@ class Uninstall(Command): self.ensure_filename('manifest') try: try: + if not self.manifest: + raise DistutilsFileError("Pass manifest with --manifest=file") f = open(self.manifest) files = [file.strip() for file in f] except IOError, e: @@ -152,7 +155,7 @@ if platform.system() == 'FreeBSD': else: man_dir = 'share/man' -setup(name='Terminator', +setup(name=APP_NAME.capitalize(), version=APP_VERSION, description='Terminator, the robot future of terminals', author='Chris Jones', @@ -173,7 +176,9 @@ setup(name='Terminator', ('share/icons/hicolor/48x48/apps', glob.glob('data/icons/48x48/apps/*.png')), ('share/icons/hicolor/16x16/actions', glob.glob('data/icons/16x16/actions/*.png')), ], - packages=['terminatorlib'], + packages=['terminatorlib', 'terminatorlib.configobj', + 'terminatorlib.plugins'], + package_data={'terminatorlib': ['preferences.glade']}, cmdclass={'build': BuildData, 'install_data': InstallData, 'uninstall': Uninstall}, distclass=TerminatorDist ) diff --git a/terminator b/terminator index 2af3cedb..3e766fc1 100755 --- a/terminator +++ b/terminator @@ -1,6 +1,6 @@ #!/usr/bin/env python # Terminator - multiple gnome terminals in one window -# Copyright (C) 2006-2008 cmsj@tenshu.net +# Copyright (C) 2006-2010 cmsj@tenshu.net # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -18,161 +18,57 @@ """Terminator by Chris Jones """ -# import standard python libs -import os, sys -origcwd = os.getcwd() -from optparse import OptionParser, SUPPRESS_HELP - -import terminatorlib.translation -from terminatorlib.version import APP_NAME, APP_VERSION - -from terminatorlib.config import dbg, err, debug -import terminatorlib.config +import sys +# Check we have simple basics like Gtk+ and a valid $DISPLAY try: - import pygtk - pygtk.require ("2.0") + import pygtk + pygtk.require ("2.0") + # pylint: disable-msg=W0611 + import gtk, pango, gobject + + if gtk.gdk.display_get_default() == None: + print('You need to run terminator in an X environment. ' \ + 'Make sure $DISPLAY is properly set') + sys.exit(1) - import gobject, gtk, pango except ImportError: - err (_("You need to install the python bindings for " \ - "gobject, gtk and pango to run Terminator.")) - sys.exit(1) - -from terminatorlib.terminator import Terminator - -if __name__ == '__main__': - def execute_cb (option, opt, value, lparser): - """Callback for use in parsing Terminator command line options""" - assert value is None - value = [] - while lparser.rargs: - arg = lparser.rargs[0] - value.append (arg) - del (lparser.rargs[0]) - setattr(lparser.values, option.dest, value) - - def profile_cb (option, opt, value, lparser): - """Callback for handling the profile name""" - assert value is None - value = '' - while lparser.rargs: - arg = lparser.rargs[0] - if arg[0] != '-': - if len (value) > 0: - value = '%s %s' % (value, arg) - else: - value = '%s' % arg - del (lparser.rargs[0]) - else: - break - setattr (lparser.values, option.dest, value) - - usage = "usage: %prog [options]" - parser = OptionParser (usage) - parser.add_option ("-v", "--version", action="store_true", dest="version", - help="Display program version") - parser.add_option ("-d", "--debug", action="count", dest="debug", - help="Enable debugging information (twice for debug server)") - parser.add_option ("-m", "--maximise", action="store_true", dest="maximise", - help="Open the %s window maximised" % APP_NAME.capitalize()) - parser.add_option ("-f", "--fullscreen", action="store_true", - dest="fullscreen", help="Set the window into fullscreen mode") - parser.add_option ("-b", "--borderless", action="store_true", - dest="borderless", help="Turn off the window's borders") - parser.add_option("-H", "--hidden", action="store_true", dest="hidden", - help="Open the %s window hidden"%APP_NAME.capitalize()) - parser.add_option("-T", "--title", dest="forcedtitle", - help="Specify a title to use for the window") - parser.add_option ("-n", "--no-gconf", dest="no_gconf", action="store_true", - help="ignore gnome-terminal gconf settings") - parser.add_option ("-p", "--profile", dest="profile", action="callback", - callback=profile_cb, help="Specify a GNOME Terminal profile to emulate") - parser.add_option ("--geometry", dest="geometry", type="string", - help="Set the preferred size and position of the window (see X man page)") - parser.add_option ("-e", "--command", dest="command", - help="Execute the argument to this option inside the terminal") - parser.add_option ("-x", "--execute", dest="execute", action="callback", - callback=execute_cb, help="Execute the remainder of the command line \ -inside the terminal") - parser.add_option ("--working-directory", metavar="DIR", - dest="working_directory", help="Set the terminal's working directory") - parser.add_option ("-r", "--role", dest="role", - help="Set custom WM_WINDOW_ROLE property") - for item in ['--sm-client-id', '--sm-config-prefix', '--screen']: - parser.add_option (item, dest="dummy", action="store", help=SUPPRESS_HELP) - - (options, args) = parser.parse_args () - if len (args) != 0: - parser.error("Expecting zero additional arguments, found: %d: %s" % (len (args), args)) - - if options.no_gconf and options.profile: - parser.error("using --no-gconf and defining a profile at the same time \ -does not make sense") - - if options.version: - print "%s %s" % (APP_NAME, APP_VERSION) - sys.exit (0) - - if options.debug: - terminatorlib.config.debug = True - - dbg ("%s starting up, version %s" % (APP_NAME, APP_VERSION)) - - command = None - if (options.command): - command = options.command - if (options.execute): - command = options.execute - - if gtk.gdk.display_get_default() == None: - err (_("You need to run terminator in an X environment. " \ - "Make sure DISPLAY is properly set")) + print('You need to install the python bindings for ' \ + 'gobject, gtk and pango to run Terminator.') sys.exit(1) - if options.working_directory: - if os.path.exists (os.path.expanduser (options.working_directory)): - os.chdir (os.path.expanduser (options.working_directory)) - else: - err (_("The working directory you specified does not exist.")) - sys.exit (1) +import terminatorlib.optionparse +from terminatorlib.terminator import Terminator +from terminatorlib.factory import Factory +from terminatorlib.version import APP_NAME, APP_VERSION +from terminatorlib.util import dbg - try: - open (os.path.expanduser ('~/.config/terminator/config')) - except IOError: - try: - open (os.path.expanduser ('~/.terminatorrc')) - error = gtk.MessageDialog (None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, - gtk.BUTTONS_OK, ('''You have a configuration file: - - ~/.terminatorrc. - -Please be aware that this file needs to be moved to: - - ~/.config/terminator/config. - -See the following bug report for more details: - - https://bugs.launchpad.net/bugs/238070''')) - error.run () - error.destroy () - except IOError: - pass - - dbg ('profile_cb: settled on profile: "%s"' % options.profile) - term = Terminator (options.profile, command, options.fullscreen, - options.maximise, options.borderless, options.no_gconf, - options.geometry, options.hidden, options.forcedtitle, options.role) - - term.origcwd = origcwd +if __name__ == '__main__': + dbg ("%s starting up, version %s" % (APP_NAME, APP_VERSION)) - if options.debug > 1: - import terminatorlib.debugserver as debugserver - import threading + OPTIONS = terminatorlib.optionparse.parse_options() - gtk.gdk.threads_init() - (debugthread, debugsvr) = debugserver.spawn(locals()) - term.debugaddress = debugsvr.server_address + MAKER = Factory() + TERMINATOR = Terminator() + TERMINATOR.reconfigure() + WINDOW = MAKER.make('Window') + TERMINAL = MAKER.make('Terminal') - gtk.main() + WINDOW.add(TERMINAL) + WINDOW.show() + TERMINAL.spawn_child() + + if OPTIONS.debug > 2: + import terminatorlib.debugserver as debugserver + # pylint: disable-msg=W0611 + import threading + + gtk.gdk.threads_init() + (DEBUGTHREAD, DEBUGSVR) = debugserver.spawn(locals()) + TERMINATOR.debugaddress = DEBUGSVR.server_address + + try: + gtk.main() + except KeyboardInterrupt: + pass diff --git a/terminator.spec b/terminator.spec index ec51b70f..69f7d366 100644 --- a/terminator.spec +++ b/terminator.spec @@ -1,7 +1,7 @@ %{!?python_sitelib: %define python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")} Name: terminator -Version: 0.14 +Version: 0.90 Release: 3%{?dist} Summary: Store and run multiple GNOME terminals in one window @@ -52,6 +52,7 @@ rm -rf %{buildroot} %{_mandir}/man5/%{name}_config.* %{_bindir}/%{name} %{python_sitelib}/* +%{_datadir}/terminator/preferences.glade %{_datadir}/applications/%{name}.desktop %{_datadir}/icons/hicolor/*/*/%{name}*.png %{_datadir}/icons/hicolor/*/*/%{name}*.svg @@ -67,6 +68,10 @@ gtk-update-icon-cache -qf %{_datadir}/icons/hicolor &>/dev/null || : %changelog +* Tue Jan 05 2010 Chris Jones 0.90-1 +- Attempt to update for 0.90 pre-release. + Note that this specfile is untested. + * Thu Jan 15 2009 Chris Jones 0.12-1 - Remove patch application since this isn't a fedora build. Note that this specfile is untested. diff --git a/terminatorlib/__init__.py b/terminatorlib/__init__.py index 21c69fda..314dc331 100644 --- a/terminatorlib/__init__.py +++ b/terminatorlib/__init__.py @@ -1,6 +1,6 @@ #!/usr/bin/python # Terminator - multiple gnome terminals in one window -# Copyright (C) 2006-2008 cmsj@tenshu.net +# Copyright (C) 2006-2010 cmsj@tenshu.net # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -16,4 +16,3 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA """Terminator by Chris Jones """ - diff --git a/terminatorlib/borg.py b/terminatorlib/borg.py new file mode 100755 index 00000000..d61e7283 --- /dev/null +++ b/terminatorlib/borg.py @@ -0,0 +1,55 @@ +#!/usr/bin/python +# Terminator by Chris Jones +# GPL v2 only +"""borg.py - We are the borg. Resistance is futile. + http://code.activestate.com/recipes/66531/ + ActiveState's policy appears to be that snippets + exist to encourage re-use, but I can not find any + specific licencing terms. +""" + +from util import dbg + +# pylint: disable-msg=R0903 +# pylint: disable-msg=R0921 +class Borg: + """Definition of a class that can never be duplicated. Correct usage is + thus: + + from borg import Borg + class foo(Borg): + # All attributes on a borg class *must* = None + attribute = None + + def __init__(self): + Borg.__init__(self, self.__class__.__name__) + + def prepare_attributes(self): + if not self.attribute: + self.attribute = [] + + bar = foo() + bar.prepare_attributes() + + The important thing to note is that all attributes of borg classes *must* be + declared as being None. If you attempt to use static class attributes you + will get unpredicted behaviour. Instead, prepare_attributes() must be called + which will then see the attributes in the shared state, and initialise them + if necessary.""" + __shared_state = {} + + def __init__(self, borgtype=None): + """Class initialiser. Overwrite our class dictionary with the shared + state. This makes us identical to every other instance of this class + type.""" + if borgtype is None: + raise TypeError('Borg::__init__: You must pass a borgtype') + if not self.__shared_state.has_key(borgtype): + dbg('Borg::__init__: Preparing borg state for %s' % borgtype) + self.__shared_state[borgtype] = {} + self.__dict__ = self.__shared_state[borgtype] + + def prepare_attributes(self): + """This should be used to prepare any attributes of the borg class.""" + raise NotImplementedError('prepare_attributes') + diff --git a/terminatorlib/config.py b/terminatorlib/config.py index 7f4d55cf..08718c9f 100755 --- a/terminatorlib/config.py +++ b/terminatorlib/config.py @@ -1,6 +1,6 @@ #!/usr/bin/python # TerminatorConfig - layered config classes -# Copyright (C) 2006-2008 cmsj@tenshu.net +# Copyright (C) 2006-2010 cmsj@tenshu.net # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -15,532 +15,513 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -"""TerminatorConfig by Chris Jones +"""Terminator by Chris Jones -The config scheme works in layers, with defaults at the base, -and a simple/flexible class which can be placed over the top -in multiple layers. This was written for Terminator, but -could be used generically. Its original use is to guarantee -default values for any config item, while allowing them to be -overridden by at least two other stores of configuration values. -Those being gconf and a plain config file. -In addition to the value, the default layer must also provide -the datatype (str, int, float and bool are currently supported). -values are found as attributes of the TerminatorConfig object. -Trying to read a value that doesn't exist will raise an -AttributeError. This is by design. If you want to look something -up, set a default for it first.""" +Classes relating to configuration + +>>> DEFAULTS['global_config']['focus'] +'click' +>>> config = Config() +>>> config['focus'] = 'sloppy' +>>> config['focus'] +'sloppy' +>>> DEFAULTS['global_config']['focus'] +'click' +>>> config2 = Config() +>>> config2['focus'] +'sloppy' +>>> config2['focus'] = 'click' +>>> config2['focus'] +'click' +>>> config['focus'] +'click' +>>> config['geometry_hinting'].__class__.__name__ +'bool' +>>> plugintest = {} +>>> plugintest['foo'] = 'bar' +>>> config.plugin_set_config('testplugin', plugintest) +>>> config.plugin_get_config('testplugin') +{'foo': 'bar'} +>>> config.plugin_get('testplugin', 'foo') +'bar' +>>> config.get_profile() +'default' +>>> config.set_profile('my_first_new_testing_profile') +>>> config.get_profile() +'my_first_new_testing_profile' +>>> config.del_profile('my_first_new_testing_profile') +>>> config.get_profile() +'default' +>>> config.list_profiles().__class__.__name__ +'list' +>>> config.options_set({}) +>>> config.options_get() +{} +>>> + +""" -import os import platform +import os import sys -import re -import pwd -import gtk -import pango - -try: - import gconf -except ImportError: - gconf = None - -from terminatorlib import translation - -# set this to true to enable debugging output -# These should be moved somewhere better. -debug = False - -def dbg (log = ""): - """Print a message if debugging is enabled""" - if debug: - print >> sys.stderr, log - -def err (log = ""): - """Print an error message""" - print >> sys.stderr, log - -from terminatorlib.configfile import ConfigFile, ParsedWithErrors +from copy import copy +from configobj.configobj import ConfigObj, flatten_errors +from configobj.validate import Validator +from borg import Borg +from util import dbg, err, DEBUG, get_config_dir, dict_diff DEFAULTS = { - 'gt_dir' : '/apps/gnome-terminal', - 'profile_dir' : '/apps/gnome-terminal/profiles', - 'titlebars' : True, - 'zoomedtitlebar' : True, - 'titletips' : False, - 'allow_bold' : True, - 'audible_bell' : False, - 'visible_bell' : True, - 'urgent_bell' : False, - 'background_color' : '#000000', - 'background_darkness' : 0.5, - 'background_type' : 'solid', - 'background_image' : '', - 'backspace_binding' : 'ascii-del', - 'delete_binding' : 'delete-sequence', - 'cursor_blink' : True, - 'cursor_shape' : 'block', - 'cursor_color' : '', - 'emulation' : 'xterm', - 'geometry_hinting' : True, - 'font' : 'Mono 10', - 'foreground_color' : '#AAAAAA', - 'scrollbar_position' : "right", - 'scroll_background' : True, - 'scroll_on_keystroke' : True, - 'scroll_on_output' : True, - 'scrollback_lines' : 500, - 'focus' : 'click', - 'exit_action' : 'close', - 'palette' : '#000000000000:#CDCD00000000:#0000CDCD0000:#CDCDCDCD0000:#30BF30BFA38E:#A53C212FA53C:#0000CDCDCDCD:#FAFAEBEBD7D7:#404040404040:#FFFF00000000:#0000FFFF0000:#FFFFFFFF0000:#00000000FFFF:#FFFF0000FFFF:#0000FFFFFFFF:#FFFFFFFFFFFF', - 'word_chars' : '-A-Za-z0-9,./?%&#:_', - 'mouse_autohide' : True, - 'update_records' : True, - 'login_shell' : False, - 'use_custom_command' : False, - 'custom_command' : '', - 'use_system_font' : True, - 'use_theme_colors' : False, - 'http_proxy' : '', - 'ignore_hosts' : ['localhost','127.0.0.0/8','*.local'], - 'encoding' : 'UTF-8', - 'active_encodings' : ['UTF-8', 'ISO-8859-1'], - 'extreme_tabs' : False, - 'fullscreen' : False, - 'borderless' : False, - 'maximise' : False, - 'hidden' : False, - 'handle_size' : -1, - 'focus_on_close' : 'auto', - 'f11_modifier' : False, - 'force_no_bell' : False, - 'cycle_term_tab' : True, - 'copy_on_selection' : False, - 'close_button_on_tab' : True, - 'tab_position' : 'top', - 'enable_real_transparency' : True, - 'title_tx_txt_color' : '#FFFFFF', - 'title_tx_bg_color' : '#C80003', - 'title_rx_txt_color' : '#FFFFFF', - 'title_rx_bg_color' : '#0076C9', - 'title_ia_txt_color' : '#000000', - 'title_ia_bg_color' : '#C0BEBF', - 'try_posix_regexp' : platform.system() != 'Linux', - 'hide_tabbar' : False, - 'scroll_tabbar' : False, - 'alternate_screen_scroll': True, - 'keybindings' : { - 'zoom_in' : 'plus', - 'zoom_out' : 'minus', - 'zoom_normal' : '0', - 'new_root_tab' : 'T', - 'new_tab' : 'T', - 'go_next' : ('N','Tab'), - 'go_prev' : ('P','Tab'), - 'go_up' : 'Up', - 'go_down' : 'Down', - 'go_left' : 'Left', - 'go_right' : 'Right', - 'split_horiz' : 'O', - 'split_vert' : 'E', - 'close_term' : 'W', - 'copy' : 'C', - 'paste' : 'V', - 'toggle_scrollbar' : 'S', - 'search' : 'F', - 'close_window' : 'Q', - 'resize_up' : 'Up', - 'resize_down' : 'Down', - 'resize_left' : 'Left', - 'resize_right' : 'Right', - 'move_tab_right' : 'Page_Down', - 'move_tab_left' : 'Page_Up', - 'toggle_zoom' : 'X', - 'scaled_zoom' : 'Z', - 'next_tab' : 'Page_Down', - 'prev_tab' : 'Page_Up', - 'switch_to_tab_1' : None, - 'switch_to_tab_2' : None, - 'switch_to_tab_3' : None, - 'switch_to_tab_4' : None, - 'switch_to_tab_5' : None, - 'switch_to_tab_6' : None, - 'switch_to_tab_7' : None, - 'switch_to_tab_8' : None, - 'switch_to_tab_9' : None, - 'switch_to_tab_10' : None, - 'full_screen' : 'F11', - 'reset' : 'R', - 'reset_clear' : 'G', - 'hide_window' : 'a', - 'group_all' : 'g', - 'ungroup_all' : 'g', - 'group_tab' : 't', - 'ungroup_tab' : 'T', - 'new_window' : 'I', - } + 'global_config': { + 'focus' : 'click', + 'enable_real_transparency' : True, + 'handle_size' : -1, + 'geometry_hinting' : True, + 'window_state' : 'normal', + 'borderless' : False, + 'tab_position' : 'top', + 'close_button_on_tab' : True, + 'hide_tabbar' : False, + 'scroll_tabbar' : False, + 'try_posix_regexp' : platform.system() != 'Linux', + 'title_transmit_fg_color' : '#ffffff', + 'title_transmit_bg_color' : '#c80003', + 'title_receive_fg_color' : '#ffffff', + 'title_receive_bg_color' : '#0076c9', + 'title_inactive_fg_color' : '#000000', + 'title_inactive_bg_color' : '#c0bebf', + 'disabled_plugins' : ['TestPlugin', 'CustomCommandsMenu'], + }, + 'keybindings': { + 'zoom_in' : 'plus', + 'zoom_out' : 'minus', + 'zoom_normal' : '0', + 'new_tab' : 't', + 'cycle_next' : 'Tab', + 'cycle_prev' : 'Tab', + 'go_next' : 'n', + 'go_prev' : 'p', + 'go_up' : 'Up', + 'go_down' : 'Down', + 'go_left' : 'Left', + 'go_right' : 'Right', + 'split_horiz' : 'o', + 'split_vert' : 'e', + 'close_term' : 'w', + 'copy' : 'c', + 'paste' : 'v', + 'toggle_scrollbar' : 's', + 'search' : 'f', + 'close_window' : 'q', + 'resize_up' : 'Up', + 'resize_down' : 'Down', + 'resize_left' : 'Left', + 'resize_right' : 'Right', + 'move_tab_right' : 'Page_Down', + 'move_tab_left' : 'Page_Up', + 'toggle_zoom' : 'x', + 'scaled_zoom' : 'z', + 'next_tab' : 'Page_Down', + 'prev_tab' : 'Page_Up', + 'switch_to_tab_1' : '', + 'switch_to_tab_2' : '', + 'switch_to_tab_3' : '', + 'switch_to_tab_4' : '', + 'switch_to_tab_5' : '', + 'switch_to_tab_6' : '', + 'switch_to_tab_7' : '', + 'switch_to_tab_8' : '', + 'switch_to_tab_9' : '', + 'switch_to_tab_10' : '', + 'full_screen' : 'F11', + 'reset' : 'r', + 'reset_clear' : 'g', + 'hide_window' : 'a', + 'group_all' : 'g', + 'ungroup_all' : 'g', + 'group_tab' : 't', + 'ungroup_tab' : 't', + 'new_window' : 'i', + }, + 'profiles': { + 'default': { + 'allow_bold' : True, + 'audible_bell' : False, + 'visible_bell' : True, + 'urgent_bell' : False, + 'background_color' : '#000000000000', + 'background_darkness' : 0.5, + 'background_type' : 'solid', + 'background_image' : None, + 'backspace_binding' : 'ascii-del', + 'delete_binding' : 'escape-sequence', + 'color_scheme' : 'grey_on_black', + 'cursor_blink' : True, + 'cursor_shape' : 'block', + 'cursor_color' : '', + 'emulation' : 'xterm', + 'font' : 'Mono 10', + 'foreground_color' : '#aaaaaaaaaaaa', + 'scrollbar_position' : "right", + 'scroll_background' : True, + 'scroll_on_keystroke' : True, + 'scroll_on_output' : True, + 'scrollback_lines' : 500, + 'exit_action' : 'close', + 'palette' :'#000000000000:#CDCD00000000:#0000CDCD0000:\ +#CDCDCDCD0000:#30BF30BFA38E:#A53C212FA53C:\ +#0000CDCDCDCD:#FAFAEBEBD7D7:#404040404040:\ +#FFFF00000000:#0000FFFF0000:#FFFFFFFF0000:\ +#00000000FFFF:#FFFF0000FFFF:#0000FFFFFFFF:\ +#FFFFFFFFFFFF', + 'word_chars' : '-A-Za-z0-9,./?%&#:_', + 'mouse_autohide' : True, + 'update_records' : True, + 'login_shell' : False, + 'use_custom_command' : False, + 'custom_command' : '', + 'use_system_font' : True, + 'use_theme_colors' : False, + 'encoding' : 'UTF-8', + 'active_encodings' : ['UTF-8', 'ISO-8859-1'], + 'focus_on_close' : 'auto', + 'force_no_bell' : False, + 'cycle_term_tab' : True, + 'copy_on_selection' : False, + 'title_tx_txt_color' : '#FFFFFF', + 'title_tx_bg_color' : '#C80003', + 'title_rx_txt_color' : '#FFFFFF', + 'title_rx_bg_color' : '#0076C9', + 'title_ia_txt_color' : '#000000', + 'title_ia_bg_color' : '#C0BEBF', + 'alternate_screen_scroll': True, + 'split_to_group' : False, + 'autoclean_groups' : True, + 'http_proxy' : '', + 'ignore_hosts' : ['localhost','127.0.0.0/8','*.local'], + }, + }, + 'layouts': { + }, + 'plugins': { + }, } - -class TerminatorConfig(object): - """This class is used as the base point of the config system""" - callback = None - sources = None - _keys = None - - def __init__ (self, sources): - self.sources = [] - - for source in sources: - if isinstance(source, TerminatorConfValuestore): - self.sources.append (source) - - # We always add a default valuestore last so no valid config item ever - # goes unset - source = TerminatorConfValuestoreDefault () - self.sources.append (source) - - def _merge_keybindings(self): - if self._keys: - return self._keys - - self._keys = {} - for source in reversed(self.sources): - try: - val = source['keybindings'] - self._keys.update(val) - except: - pass - return self._keys - - keybindings = property(_merge_keybindings) - - def __getattr__ (self, keyname): - for source in self.sources: - dbg ("TConfig: Looking for: '%s' in '%s'"%(keyname, source.type)) - try: - val = source[keyname] - dbg (" TConfig: got: '%s' from a '%s'"%(val, source.type)) - return (val) - except KeyError: - pass - - dbg (" TConfig: Out of sources") - raise (AttributeError) - -class TerminatorConfValuestore(object): - type = "Base" - values = None - reconfigure_callback = None - - def __init__ (self): - self.values = {} - - # Our settings - def __getitem__ (self, keyname): - if self.values.has_key (keyname): - value = self.values[keyname] - dbg ("Returning '%s':'%s'"%(keyname, value)) - return value - else: - dbg ("Failed to find '%s'"%keyname) - raise (KeyError) - -class TerminatorConfValuestoreDefault (TerminatorConfValuestore): - def __init__ (self): - TerminatorConfValuestore.__init__ (self) - self.type = "Default" - self.values = DEFAULTS - -class TerminatorConfValuestoreRC (TerminatorConfValuestore): - rcfilename = "" - type = "RCFile" - def __init__ (self): - TerminatorConfValuestore.__init__ (self) - try: - directory = os.environ['XDG_CONFIG_HOME'] - except KeyError: - dbg(" VS_RCFile: XDG_CONFIG_HOME not found. defaulting to ~/.config") - directory = os.path.join (os.path.expanduser("~"), ".config") - self.rcfilename = os.path.join(directory, "terminator/config") - dbg(" VS_RCFile: config file located at %s" % self.rcfilename) - self.call_parser(True) - - def set_reconfigure_callback (self, function): - dbg (" VS_RCFile: setting callback to: %s"%function) - self.reconfigure_callback = function - return (True) - - def call_parser (self, is_init = False): - dbg (" VS_RCFile: parsing config file") - try: - ini = ConfigFile(self.rcfilename, self._rc_set_callback()) - ini.parse() - except IOError, ex: - dbg (" VS_RCFile: unable to open %s (%r)" % (self.rcfilename, ex)) - except ParsedWithErrors, ex: - # We don't really want to produce an error dialog every run - if not is_init: - pass - msg = _("""Configuration error - -Errors were encountered while parsing terminator_config(5) file: - - %s - -%d line(s) have been ignored.""") % (self.rcfilename, len(ex.errors)) - - dialog = gtk.Dialog(_("Configuration error"), None, gtk.DIALOG_MODAL, - (gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) - dialog.set_has_separator(False) - dialog.set_resizable(False) - - image = gtk.image_new_from_stock(gtk.STOCK_DIALOG_WARNING, - gtk.ICON_SIZE_DIALOG) - image.set_alignment (0.5, 0) - dmsg = gtk.Label(msg) - dmsg.set_use_markup(True) - dmsg.set_alignment(0, 0.5) - - textbuff = gtk.TextBuffer() - textbuff.set_text("\n".join(map(lambda ex: str(ex), ex.errors))) - textview = gtk.TextView(textbuff) - textview.set_editable(False) - - textview.modify_font(pango.FontDescription(DEFAULTS['font'])) - textscroll = gtk.ScrolledWindow() - textscroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) - textscroll.add(textview) - # This should be scaled with the size of the text and font - textscroll.set_size_request(600, 200) - - root = gtk.VBox() - root.pack_start(dmsg, padding = 6) - root.pack_start(textscroll, padding = 6) - - box = gtk.HBox() - box.pack_start (image, False, False, 6) - box.pack_start (root, False, False, 6) - - vbox = dialog.get_content_area() - vbox.pack_start (box, False, False, 12) - dialog.show_all() - - dialog.run() - dialog.destroy() - - dbg("ConfigFile settings are: %r" % self.values) - - def _rc_set_callback(self): - def callback(sections, key, value): - dbg("Setting: section=%r with %r => %r" % (sections, key, value)) - section = None - if len(sections) > 0: - section = sections[0] - if section is None: - # handle some deprecated configs - if key == 'silent_bell': - err ("silent_bell config option is deprecated, for the new bell related config options, see: man terminator_config") - if value: - self.values['audible_bell'] = False - else: - self.values['audible_bell'] = True - key = 'visible_bell' - - if not DEFAULTS.has_key (key): - raise ValueError("Unknown configuration option %r" % key) - deftype = DEFAULTS[key].__class__.__name__ - if key.endswith('_color'): - try: - gtk.gdk.color_parse(value) - self.values[key] = value - except ValueError: - raise ValueError(_("Setting %r value %r not a valid colour; ignoring") % (key, value)) - elif key == 'tab_position': - if value.lower() in ('top', 'left', 'bottom', 'right'): - self.values[key] = value.lower() - else: - raise ValueError(_("%s must be one of: top, left, right, bottom") % key) - elif deftype == 'bool': - if value.lower () in ('true', 'yes', 'on'): - self.values[key] = True - elif value.lower () in ('false', 'no', 'off'): - self.values[key] = False - else: - raise ValueError(_("Boolean setting %s expecting one of: yes, no, true, false, on, off") % key) - elif deftype == 'int': - self.values[key] = int (value) - elif deftype == 'float': - self.values[key] = float (value) - elif deftype == 'list': - raise ValueError(_("Reading list values from terminator_config(5) is not currently supported")) - elif deftype == 'dict': - if type(value) != dict: - raise ValueError(_("Setting %r should be a section name") % key) - self.values[key] = value - else: - self.values[key] = value - - dbg (" VS_RCFile: Set value %r to %r" % (key, self.values[key])) - elif section == 'keybindings': - self.values.setdefault(section, {}) - if not DEFAULTS[section].has_key(key): - raise ValueError("Keybinding name %r is unknown" % key) - else: - self.values[section][key] = value - else: - raise ValueError("Section name %r is unknown" % section) - return callback - -class TerminatorConfValuestoreGConf (TerminatorConfValuestore): - profile = "" - client = None - cache = None - notifies = None - - def __init__ (self, profileName = None): - TerminatorConfValuestore.__init__ (self) - self.type = "GConf" - self.inactive = False - self.cache = {} - self.notifies = {} - - import gconf - - self.client = gconf.client_get_default () - - # Grab a couple of values from base class to avoid recursing with our __getattr__ - self._gt_dir = DEFAULTS['gt_dir'] - self._profile_dir = DEFAULTS['profile_dir'] - - dbg ('VSGConf: Profile bet on is: "%s"'%profileName) - profiles = self.client.get_list (self._gt_dir + '/global/profile_list','string') - dbg ('VSGConf: Found profiles: "%s"'%profiles) - - dbg ('VSGConf: Profile requested is: "%s"'%profileName) - if not profileName: - profile = self.client.get_string (self._gt_dir + '/global/default_profile') - else: - profile = profileName - # In newer gnome-terminal, the profile keys are named Profile0/1 etc. - # We have to match using visible_name instead - for p in profiles: - profileName2 = self.client.get_string ( - self._profile_dir + '/' + p + '/visible_name') - if profileName == profileName2: - profile = p - - #need to handle the list of Gconf.value - if profile in profiles: - dbg (" VSGConf: Found profile '%s' in profile_list"%profile) - self.profile = '%s/%s' % (self._profile_dir, profile) - elif "Default" in profiles: - dbg (" VSGConf: profile '%s' not found, but 'Default' exists" % profile) - self.profile = '%s/%s'%(self._profile_dir, "Default") - else: - # We're a bit stuck, there is no profile in the list - # FIXME: Find a better way to handle this than setting a non-profile - dbg ("VSGConf: No profile found, marking inactive") - self.inactive = True - return - - #set up the active encoding list - self.active_encodings = self.client.get_list (self._gt_dir + '/global/active_encodings', 'string') +class Config(object): + """Class to provide a slightly richer config API above ConfigBase""" + base = None + profile = None - self.client.add_dir (self.profile, gconf.CLIENT_PRELOAD_RECURSIVE) - if self.on_gconf_notify: - self.client.notify_add (self.profile, self.on_gconf_notify) + def __init__(self, profile='default'): + self.base = ConfigBase() + self.profile = profile - self.client.add_dir ('/apps/metacity/general', gconf.CLIENT_PRELOAD_RECURSIVE) - self.client.notify_add ('/apps/metacity/general/focus_mode', self.on_gconf_notify) - self.client.add_dir ('/desktop/gnome/interface', gconf.CLIENT_PRELOAD_RECURSIVE) - self.client.notify_add ('/desktop/gnome/interface/monospace_font_name', self.on_gconf_notify) - # FIXME: Do we need to watch more non-profile stuff here? + def __getitem__(self, key): + """Look up a configuration item""" + return(self.base.get_item(key, self.profile)) - def set_reconfigure_callback (self, function): - dbg (" VSConf: setting callback to: %s"%function) - self.reconfigure_callback = function - return (True) + def __setitem__(self, key, value): + """Set a particular configuration item""" + return(self.base.set_item(key, value, self.profile)) - def on_gconf_notify (self, client, cnxn_id, entry, what): - dbg (" VSGConf: invalidating cache") - self.cache = {} - dbg (" VSGConf: gconf changed, may run a callback. %s, %s"%(entry.key, entry.value)) - if entry.key[-12:] == 'visible_name': - dbg (" VSGConf: only a visible_name change, ignoring") - return False - if self.reconfigure_callback: - dbg (" VSGConf: callback is: %s"%self.reconfigure_callback) - self.reconfigure_callback () + def get_profile(self): + """Get our profile""" + return(self.profile) - def __getitem__ (self, key = ""): - if self.inactive: - raise KeyError + def set_profile(self, profile): + """Set our profile (which usually means change it)""" + dbg('Config::set_profile: Changing profile to %s' % profile) + self.profile = profile + if not self.base.profiles.has_key(profile): + dbg('Config::set_profile: %s does not exist, creating' % profile) + self.base.profiles[profile] = copy(DEFAULTS['profiles']['default']) - if self.cache.has_key (key): - dbg (" VSGConf: returning cached value: %s"%self.cache[key]) - return (self.cache[key]) + def add_profile(self, profile): + """Add a new profile""" + return(self.base.add_profile(profile)) - ret = None - value = None + def del_profile(self, profile): + """Delete a profile""" + if profile == self.profile: + err('Config::del_profile: Deleting in-use profile %s.' % profile) + self.set_profile('default') + if self.base.profiles.has_key(profile): + del(self.base.profiles[profile]) - dbg (' VSGConf: preparing: %s/%s'%(self.profile, key)) + def rename_profile(self, profile, newname): + """Rename a profile""" + if self.base.profiles.has_key(profile): + self.base.profiles[newname] = self.base.profiles[profile] + del(self.base.profiles[profile]) + if profile == self.profile: + self.profile = newname - # FIXME: Ugly special cases we should look to fix in some other way. - if key == 'font' and self['use_system_font']: - value = self.client.get ('/desktop/gnome/interface/monospace_font_name') - elif key == 'focus': - value = self.client.get ('/apps/metacity/general/focus_mode') - elif key == 'http_proxy': - if self.client.get_bool ('/system/http_proxy/use_http_proxy'): - dbg ('HACK: Mangling http_proxy') + def list_profiles(self): + """List all configured profiles""" + return(self.base.profiles.keys()) - if self.client.get_bool ('/system/http_proxy/use_authentication'): - dbg ('HACK: Using proxy authentication') - value = 'http://%s:%s@%s:%s/' % ( - self.client.get_string ('/system/http_proxy/authentication_user'), - self.client.get_string ('/system/http_proxy/authentication_password'), - self.client.get_string ('/system/http_proxy/host'), - self.client.get_int ('/system/http_proxy/port')) + def save(self): + """Cause ConfigBase to save our config to file""" + return(self.base.save()) + + def options_set(self, options): + """Set the command line options""" + self.base.command_line_options = options + + def options_get(self): + """Get the command line options""" + return(self.base.command_line_options) + + def plugin_get(self, pluginname, key): + """Get a plugin config value""" + return(self.base.get_item(key, plugin=pluginname)) + + def plugin_set(self, pluginname, key, value): + """Set a plugin config value""" + return(self.base.set_item(key, value, plugin=pluginname)) + + def plugin_get_config(self, plugin): + """Return a whole config tree for a given plugin""" + return(self.base.get_plugin(plugin)) + + def plugin_set_config(self, plugin, tree): + """Set a whole config tree for a given plugin""" + return(self.base.set_plugin(plugin, tree)) + +class ConfigBase(Borg): + """Class to provide access to our user configuration""" + loaded = None + sections = None + global_config = None + profiles = None + keybindings = None + plugins = None + layouts = None + command_line_options = None + + def __init__(self): + """Class initialiser""" + + Borg.__init__(self, self.__class__.__name__) + + self.prepare_attributes() + self.load() + + def prepare_attributes(self): + """Set up our borg environment""" + if self.loaded is None: + self.loaded = False + if self.sections is None: + self.sections = ['global_config', 'keybindings', 'profiles', + 'layouts', 'plugins'] + if self.global_config is None: + self.global_config = copy(DEFAULTS['global_config']) + if self.profiles is None: + self.profiles = {} + self.profiles['default'] = copy(DEFAULTS['profiles']['default']) + if self.keybindings is None: + self.keybindings = copy(DEFAULTS['keybindings']) + if self.plugins is None: + self.plugins = {} + if self.layouts is None: + self.layouts = copy(DEFAULTS['layouts']) + + def defaults_to_configspec(self): + """Convert our tree of default values into a ConfigObj validation + specification""" + configspecdata = {} + + section = {} + for key in DEFAULTS['global_config']: + keytype = DEFAULTS['global_config'][key].__class__.__name__ + value = DEFAULTS['global_config'][key] + if keytype == 'int': + keytype = 'integer' + elif keytype == 'str': + keytype = 'string' + elif keytype == 'bool': + keytype = 'boolean' + elif keytype == 'list': + value = 'list(%s)' % ','.join(value) + + keytype = '%s(default=%s)' % (keytype, value) + + section[key] = keytype + configspecdata['global_config'] = section + + section = {} + for key in DEFAULTS['keybindings']: + value = DEFAULTS['keybindings'][key] + if value is None or value == '': + continue + section[key] = 'string(default=%s)' % value + configspecdata['keybindings'] = section + + section = {} + for key in DEFAULTS['profiles']['default']: + keytype = DEFAULTS['profiles']['default'][key].__class__.__name__ + value = DEFAULTS['profiles']['default'][key] + if keytype == 'int': + keytype = 'integer' + elif keytype == 'bool': + keytype = 'boolean' + elif keytype == 'str': + keytype = 'string' + value = '"%s"' % value + elif keytype == 'list': + value = 'list(%s)' % ','.join(value) + + keytype = '%s(default=%s)' % (keytype, value) + + section[key] = keytype + configspecdata['profiles'] = {} + configspecdata['profiles']['__many__'] = section + + configspec = ConfigObj(configspecdata) + if DEBUG == True: + configspec.write(open('/tmp/terminator_configspec_debug.txt', 'w')) + return(configspec) + + def load(self): + """Load configuration data from our various sources""" + if self.loaded is True: + dbg('ConfigBase::load: config already loaded') + return + + filename = os.path.join(get_config_dir(), 'epic-config') + try: + configfile = open(filename, 'r') + except Exception, ex: + dbg('ConfigBase::load: Unable to open %s (%s)' % (filename, ex)) + return + + configspec = self.defaults_to_configspec() + parser = ConfigObj(configfile, configspec=configspec) + validator = Validator() + result = parser.validate(validator, preserve_errors=True) + + if result != True: + err('ConfigBase::load: config format is not valid') + for (section_list, key, other) in flatten_errors(parser, result): + if key is not None: + print('[%s]: %s is invalid' % (','.join(section_list), key)) + else: + print ('[%s] missing' % ','.join(section_list)) + + for section_name in self.sections: + dbg('ConfigBase::load: Processing section: %s' % section_name) + section = getattr(self, section_name) + if section_name == 'profiles': + for profile in parser[section_name]: + dbg('ConfigBase::load: Processing profile: %s' % profile) + if not section.has_key(section_name): + section[profile] = copy(DEFAULTS['profiles']['default']) + section[profile].update(parser[section_name][profile]) + elif section_name == ['layouts', 'plugins']: + for part in parser[section_name]: + dbg('ConfigBase::load: Processing %s: %s' % (section_name, + part)) + section[part] = parser[section_name][part] + else: + try: + section.update(parser[section_name]) + except KeyError, ex: + dbg('ConfigBase::load: skipping loading missing section %s' % + section_name) + + self.loaded = True + + def save(self): + """Save the config to a file""" + dbg('ConfigBase::save: saving config') + parser = ConfigObj() + parser.indent_type = ' ' + + for section_name in ['global_config', 'keybindings']: + dbg('ConfigBase::save: Processing section: %s' % section_name) + section = getattr(self, section_name) + parser[section_name] = dict_diff(DEFAULTS[section_name], section) + + parser['profiles'] = {} + for profile in self.profiles: + dbg('ConfigBase::save: Processing profile: %s' % profile) + parser['profiles'][profile] = dict_diff(DEFAULTS['profiles']['default'], + self.profiles[profile]) + + parser['layouts'] = {} + for layout in self.layouts: + dbg('ConfigBase::save: Processing layout: %s' % layout) + parser['layouts'][layout] = self.layouts[layout] + + parser['plugins'] = {} + for plugin in self.plugins: + dbg('ConfigBase::save: Processing plugin: %s' % plugin) + parser['plugins'][plugin] = self.plugins[plugin] + + config_dir = get_config_dir() + if not os.path.isdir(config_dir): + os.makedirs(config_dir) + try: + parser.write(open(os.path.join(config_dir, 'epic-config'), 'w')) + except Exception, ex: + err('ConfigBase::save: Unable to save config: %s' % ex) + + def get_item(self, key, profile='default', plugin=None): + """Look up a configuration item""" + if self.global_config.has_key(key): + dbg('ConfigBase::get_item: %s found in globals: %s' % + (key, self.global_config[key])) + return(self.global_config[key]) + elif self.profiles[profile].has_key(key): + dbg('ConfigBase::get_item: %s found in profile %s: %s' % ( + key, profile, self.profiles[profile][key])) + return(self.profiles[profile][key]) + elif key == 'keybindings': + return(self.keybindings) + elif plugin is not None and self.plugins[plugin].has_key(key): + dbg('ConfigBase::get_item: %s found in plugin %s: %s' % ( + key, plugin, self.plugins[plugin][key])) + return(self.plugins[plugin][key]) else: - dbg ('HACK: Not using proxy authentication') - value = 'http://%s:%s/' % ( - self.client.get_string ('/system/http_proxy/host'), - self.client.get_int ('/system/http_proxy/port')) - elif key == 'cursor_blink': - tmp = self.client.get_string('%s/cursor_blink_mode' % self.profile) - if tmp in ['on', 'off'] and self.notifies.has_key ('cursor_blink'): - self.client.notify_remove (self.notifies['cursor_blink']) - del (self.notifies['cursor_blink']) - if tmp == 'on': - value = True - elif tmp == 'off': - value = False - elif tmp == 'system': - value = self.client.get_bool ('/desktop/gnome/interface/cursor_blink') - self.notifies['cursor_blink'] = self.client.notify_add ('/desktop/gnome/interface/cursor_blink', self.on_gconf_notify) - else: - value = self.client.get ('%s/%s'%(self.profile, key)) - else: - value = self.client.get ('%s/%s'%(self.profile, key)) + raise KeyError('ConfigBase::get_item: unknown key %s' % key) - if value != None: - from types import StringType, BooleanType - if type(value) in [StringType, BooleanType]: - ret = value - else: - funcname = "get_" + DEFAULTS[key].__class__.__name__ - dbg (' GConf: picked function: %s'%funcname) - # Special case for str - if funcname == "get_str": - funcname = "get_string" - # Special case for strlist - if funcname == "get_strlist": - funcname = "get_list" - typefunc = getattr (value, funcname) - ret = typefunc () + def set_item(self, key, value, profile='default', plugin=None): + """Set a configuration item""" + dbg('ConfigBase::set_item: Setting %s=%s (profile=%s, plugin=%s)' % + (key, value, profile, plugin)) - self.cache[key] = ret - return (ret) - else: - raise (KeyError) + if self.global_config.has_key(key): + self.global_config[key] = value + elif self.profiles[profile].has_key(key): + self.profiles[profile][key] = value + elif key == 'keybindings': + self.keybindings = value + elif plugin is not None: + if not self.plugins.has_key(plugin): + self.plugins[plugin] = {} + self.plugins[plugin][key] = value + else: + raise KeyError('ConfigBase::set_item: unknown key %s' % key) + return(True) + + def get_plugin(self, plugin): + """Return a whole tree for a plugin""" + if self.plugins.has_key(plugin): + return(self.plugins[plugin]) + + def set_plugin(self, plugin, tree): + """Set a whole tree for a plugin""" + self.plugins[plugin] = tree + + def add_profile(self, profile): + """Add a new profile""" + if profile in self.profiles: + return(False) + self.profiles[profile] = copy(DEFAULTS['profiles']['default']) + return(True) + +if __name__ == '__main__': + import doctest + (failed, attempted) = doctest.testmod() + print "%d/%d tests failed" % (failed, attempted) + sys.exit(failed) diff --git a/terminatorlib/configfile.py b/terminatorlib/configfile.py deleted file mode 100644 index f71f39f0..00000000 --- a/terminatorlib/configfile.py +++ /dev/null @@ -1,233 +0,0 @@ -#!/usr/bin/python - -import re -from terminatorlib.config import dbg, debug -from terminatorlib import translation - -def group(*choices): return '(' + '|'.join(choices) + ')' -def any(*choices): return group(*choices) + '*' -def maybe(*choices): return group(*choices) + '?' - -Newline = re.compile(r'[\r\n]+') -Whitespace = r'[ \f\t]*' -Comment = r'#[^\r\n]*' -Ignore = re.compile(Whitespace + maybe(Comment) + maybe(r'[\r\n]+') + '$') - -WhitespaceRE = re.compile(Whitespace) -CommentRE = re.compile(Comment) - -QuotedStrings = {"'": re.compile(r"'([^'\r\n]*)'"), '"': re.compile(r'"([^"\r\n]*)"')} - -Section = re.compile(r"\[([^\r\n\]]+)\][ \f\t]*") -Setting = re.compile(r"(\w+)\s*=\s*") - -PaletteColours = '(?:#[0-9a-fA-F]{12}:){15}#[0-9a-fA-F]{12}' -SingleColour = '#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3}' - -Colourvalue = re.compile(group(PaletteColours, SingleColour)) -Barevalue = re.compile(r'((?:[^\r\n# \f\t]+|[^\r\n#]+(?!' + Ignore.pattern +'))+)') - -Tabsize = 8 -HandleIndents = False - -class ConfigSyntaxError(Exception): - def __init__(self, message, cf): - self.single_error = cf.errors_are_fatal - self.message = message - self.file = cf.filename - self.lnum = cf._lnum - self.pos = cf._pos - self.line = cf._line - - def __str__(self): - if self.single_error: - fmt = "File %(file)s line %(lnum)d:\n %(line)s\n %(pad)s^\n%(message)s" - else: - fmt = " * %(message)s, line %(lnum)d:\n %(line)s\n %(pad)s^\n" - return fmt % {'message': self.message, 'file': self.file, 'lnum': self.lnum, - 'line': self.line.rstrip(), 'pad': '-' * self.pos} - -class ConfigIndentError(ConfigSyntaxError): - pass - -class ParsedWithErrors(Exception): - def __init__(self, filename, errors): - self.file = filename - self.errors = errors - - def __str__(self): - return """Errors were encountered while parsing configuration file: - - %r - -Some lines have been ignored. - -%s -""" % (self.file, "\n".join(map(lambda error: str(error), self.errors))) - - -class ConfigFile: - def __init__(self, filename = None, callback = None, errors_are_fatal = False): - self.callback = callback - self.errors_are_fatal = errors_are_fatal - self.filename = filename - self.errors = [] - - def _call_if_match(self, re, callable, group = 0): - if self._pos == self._max: - return False - mo = re.match(self._line, self._pos) - if mo: - if callable: - callable(mo.group(group)) - self._pos = mo.end() - return True - else: - return False - - def _call_if_quoted_string(self, callable): - if self._pos == self._max: - return False - chr = self._line[self._pos] - if chr in '"\'': - string = '' - while True: - mo = QuotedStrings[chr].match(self._line, self._pos) - if mo is None: - raise ConfigSyntaxError(_("Unterminated quoted string"), self) - self._pos = mo.end() - if self._line[self._pos - 2] == '\\': - string += mo.group(1)[0:-1] + chr - self._pos -= 1 - else: - string += mo.group(1) - break - callable(string) - return True - else: - return False - - def parse(self): - file = open(self.filename) - rc = file.readlines() - file.close() - - self._indents = [0] - self._pos = 0 - self._max = 0 - self._lnum = 0 - self._line = '' - - self._sections = {} - self._currsetting = None - self._currvalue = None - self.errors = [] - - for self._line in rc: - try: - self._lnum += 1 - self._pos = 0 - self._max = len(self._line) - dbg("Line %d: %r" % (self._lnum, self._line)) - - if HandleIndents: - self._find_indent() - else: - self._call_if_match(WhitespaceRE, None) - - # [Section] - self._call_if_match(Section, self._section, 1) - # setting = - if self._call_if_match(Setting, self._setting, 1): - # "quoted value" - if not self._call_if_quoted_string(self._value): - # #000000 # colour that would otherwise be a comment - if not self._call_if_match(Colourvalue, self._value, 1): - # bare value - if not self._call_if_match(Barevalue, self._value, 1): - raise ConfigSyntaxError(_("Setting without a value"), self) - - self._call_if_match(Ignore, lambda junk: dbg("Skipping: %r" % junk)) - - if self._line[self._pos:] != '': - raise ConfigSyntaxError(_("Unexpected token"), self) - self._line_ok() - except ConfigSyntaxError, e: - self._line_error(e) - except ConfigIndentError, e: - self.errors.append(e) - break - - if self.errors: - raise ParsedWithErrors(self.filename, self.errors) - - def _find_indent(self): - # Based on tokenizer.py in the base Python standard library - column = 0 - while self._pos < self._max: - chr = self._line[self._pos] - if chr == ' ': column += 1 - elif chr == '\t': column = (column / Tabsize + 1) * Tabsize - elif chr == '\f': column = 0 - else: break - self._pos += 1 - if self._pos == self._max: return - - if column > self._indents[-1]: - self._indents.append(column) - self._indent() # self._line[:self._pos]) - - while column < self._indents[-1]: - if column not in self._indents: - raise ConfigSyntaxError("Unindent does not match a previous indent, config parsing aborted", self) - self._indents.pop() - self._deindent() - - def _indent(self): - dbg(" -> Indent %d" % len(self._indents)) - - def _deindent(self): - dbg(" -> Deindent %d" % len(self._indents)) - - def _get_section(self): - i = 1 - sections = [] - while i <= len(self._indents): - sname = self._sections.get(i, None) - if not sname: - break - sections.append(str(sname)) - i += 1 - return tuple(sections) - - def _section(self, section): - dbg("Section %r" % section) - self._sections[len(self._indents)] = section.lower() - - def _setting(self, setting): - dbg("Setting %r" % setting) - self._currsetting = setting.lower() - - def _value(self, value): - dbg("Value %r" % value) - self._currvalue = value - - def _line_ok(self): - if self._currvalue is None: return - else: - try: # *glares at 2.4 users* - try: - self.callback(self._get_section(), self._currsetting, self._currvalue) - except ValueError, e: - raise ConfigSyntaxError(str(e), self) - finally: - self._currvalue = None - - def _line_error(self, e): - self._currvalue = None - if self.errors_are_fatal: - raise e - else: - self.errors.append(e) - - diff --git a/terminatorlib/configobj/__init__.py b/terminatorlib/configobj/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/terminatorlib/configobj/configobj.py b/terminatorlib/configobj/configobj.py new file mode 100644 index 00000000..fd61bcd2 --- /dev/null +++ b/terminatorlib/configobj/configobj.py @@ -0,0 +1,2455 @@ +# configobj.py +# A config file reader/writer that supports nested sections in config files. +# Copyright (C) 2005-2009 Michael Foord, Nicola Larosa +# E-mail: fuzzyman AT voidspace DOT org DOT uk +# nico AT tekNico DOT net + +# ConfigObj 4 +# http://www.voidspace.org.uk/python/configobj.html + +# Released subject to the BSD License +# Please see http://www.voidspace.org.uk/python/license.shtml + +# Scripts maintained at http://www.voidspace.org.uk/python/index.shtml +# For information about bugfixes, updates and support, please join the +# ConfigObj mailing list: +# http://lists.sourceforge.net/lists/listinfo/configobj-develop +# Comments, suggestions and bug reports welcome. + + +from __future__ import generators + +import sys +import os +import re + +compiler = None +try: + import compiler +except ImportError: + # for IronPython + pass + + +try: + from codecs import BOM_UTF8, BOM_UTF16, BOM_UTF16_BE, BOM_UTF16_LE +except ImportError: + # Python 2.2 does not have these + # UTF-8 + BOM_UTF8 = '\xef\xbb\xbf' + # UTF-16, little endian + BOM_UTF16_LE = '\xff\xfe' + # UTF-16, big endian + BOM_UTF16_BE = '\xfe\xff' + if sys.byteorder == 'little': + # UTF-16, native endianness + BOM_UTF16 = BOM_UTF16_LE + else: + # UTF-16, native endianness + BOM_UTF16 = BOM_UTF16_BE + +# A dictionary mapping BOM to +# the encoding to decode with, and what to set the +# encoding attribute to. +BOMS = { + BOM_UTF8: ('utf_8', None), + BOM_UTF16_BE: ('utf16_be', 'utf_16'), + BOM_UTF16_LE: ('utf16_le', 'utf_16'), + BOM_UTF16: ('utf_16', 'utf_16'), + } +# All legal variants of the BOM codecs. +# TODO: the list of aliases is not meant to be exhaustive, is there a +# better way ? +BOM_LIST = { + 'utf_16': 'utf_16', + 'u16': 'utf_16', + 'utf16': 'utf_16', + 'utf-16': 'utf_16', + 'utf16_be': 'utf16_be', + 'utf_16_be': 'utf16_be', + 'utf-16be': 'utf16_be', + 'utf16_le': 'utf16_le', + 'utf_16_le': 'utf16_le', + 'utf-16le': 'utf16_le', + 'utf_8': 'utf_8', + 'u8': 'utf_8', + 'utf': 'utf_8', + 'utf8': 'utf_8', + 'utf-8': 'utf_8', + } + +# Map of encodings to the BOM to write. +BOM_SET = { + 'utf_8': BOM_UTF8, + 'utf_16': BOM_UTF16, + 'utf16_be': BOM_UTF16_BE, + 'utf16_le': BOM_UTF16_LE, + None: BOM_UTF8 + } + + +def match_utf8(encoding): + return BOM_LIST.get(encoding.lower()) == 'utf_8' + + +# Quote strings used for writing values +squot = "'%s'" +dquot = '"%s"' +noquot = "%s" +wspace_plus = ' \r\n\v\t\'"' +tsquot = '"""%s"""' +tdquot = "'''%s'''" + +try: + enumerate +except NameError: + def enumerate(obj): + """enumerate for Python 2.2.""" + i = -1 + for item in obj: + i += 1 + yield i, item + +# Sentinel for use in getattr calls to replace hasattr +MISSING = object() + +__version__ = '4.6.0' + +__revision__ = '$Id: configobj.py 156 2006-01-31 14:57:08Z fuzzyman $' + +__docformat__ = "restructuredtext en" + +__all__ = ( + '__version__', + 'DEFAULT_INDENT_TYPE', + 'DEFAULT_INTERPOLATION', + 'ConfigObjError', + 'NestingError', + 'ParseError', + 'DuplicateError', + 'ConfigspecError', + 'ConfigObj', + 'SimpleVal', + 'InterpolationError', + 'InterpolationLoopError', + 'MissingInterpolationOption', + 'RepeatSectionError', + 'ReloadError', + 'UnreprError', + 'UnknownType', + '__docformat__', + 'flatten_errors', +) + +DEFAULT_INTERPOLATION = 'configparser' +DEFAULT_INDENT_TYPE = ' ' +MAX_INTERPOL_DEPTH = 10 + +OPTION_DEFAULTS = { + 'interpolation': True, + 'raise_errors': False, + 'list_values': True, + 'create_empty': False, + 'file_error': False, + 'configspec': None, + 'stringify': True, + # option may be set to one of ('', ' ', '\t') + 'indent_type': None, + 'encoding': None, + 'default_encoding': None, + 'unrepr': False, + 'write_empty_values': False, +} + + + +def getObj(s): + s = "a=" + s + if compiler is None: + raise ImportError('compiler module not available') + p = compiler.parse(s) + return p.getChildren()[1].getChildren()[0].getChildren()[1] + + +class UnknownType(Exception): + pass + + +class Builder(object): + + def build(self, o): + m = getattr(self, 'build_' + o.__class__.__name__, None) + if m is None: + raise UnknownType(o.__class__.__name__) + return m(o) + + def build_List(self, o): + return map(self.build, o.getChildren()) + + def build_Const(self, o): + return o.value + + def build_Dict(self, o): + d = {} + i = iter(map(self.build, o.getChildren())) + for el in i: + d[el] = i.next() + return d + + def build_Tuple(self, o): + return tuple(self.build_List(o)) + + def build_Name(self, o): + if o.name == 'None': + return None + if o.name == 'True': + return True + if o.name == 'False': + return False + + # An undefined Name + raise UnknownType('Undefined Name') + + def build_Add(self, o): + real, imag = map(self.build_Const, o.getChildren()) + try: + real = float(real) + except TypeError: + raise UnknownType('Add') + if not isinstance(imag, complex) or imag.real != 0.0: + raise UnknownType('Add') + return real+imag + + def build_Getattr(self, o): + parent = self.build(o.expr) + return getattr(parent, o.attrname) + + def build_UnarySub(self, o): + return -self.build_Const(o.getChildren()[0]) + + def build_UnaryAdd(self, o): + return self.build_Const(o.getChildren()[0]) + + +_builder = Builder() + + +def unrepr(s): + if not s: + return s + return _builder.build(getObj(s)) + + + +class ConfigObjError(SyntaxError): + """ + This is the base class for all errors that ConfigObj raises. + It is a subclass of SyntaxError. + """ + def __init__(self, message='', line_number=None, line=''): + self.line = line + self.line_number = line_number + SyntaxError.__init__(self, message) + + +class NestingError(ConfigObjError): + """ + This error indicates a level of nesting that doesn't match. + """ + + +class ParseError(ConfigObjError): + """ + This error indicates that a line is badly written. + It is neither a valid ``key = value`` line, + nor a valid section marker line. + """ + + +class ReloadError(IOError): + """ + A 'reload' operation failed. + This exception is a subclass of ``IOError``. + """ + def __init__(self): + IOError.__init__(self, 'reload failed, filename is not set.') + + +class DuplicateError(ConfigObjError): + """ + The keyword or section specified already exists. + """ + + +class ConfigspecError(ConfigObjError): + """ + An error occured whilst parsing a configspec. + """ + + +class InterpolationError(ConfigObjError): + """Base class for the two interpolation errors.""" + + +class InterpolationLoopError(InterpolationError): + """Maximum interpolation depth exceeded in string interpolation.""" + + def __init__(self, option): + InterpolationError.__init__( + self, + 'interpolation loop detected in value "%s".' % option) + + +class RepeatSectionError(ConfigObjError): + """ + This error indicates additional sections in a section with a + ``__many__`` (repeated) section. + """ + + +class MissingInterpolationOption(InterpolationError): + """A value specified for interpolation was missing.""" + + def __init__(self, option): + InterpolationError.__init__( + self, + 'missing option "%s" in interpolation.' % option) + + +class UnreprError(ConfigObjError): + """An error parsing in unrepr mode.""" + + + +class InterpolationEngine(object): + """ + A helper class to help perform string interpolation. + + This class is an abstract base class; its descendants perform + the actual work. + """ + + # compiled regexp to use in self.interpolate() + _KEYCRE = re.compile(r"%\(([^)]*)\)s") + + def __init__(self, section): + # the Section instance that "owns" this engine + self.section = section + + + def interpolate(self, key, value): + def recursive_interpolate(key, value, section, backtrail): + """The function that does the actual work. + + ``value``: the string we're trying to interpolate. + ``section``: the section in which that string was found + ``backtrail``: a dict to keep track of where we've been, + to detect and prevent infinite recursion loops + + This is similar to a depth-first-search algorithm. + """ + # Have we been here already? + if backtrail.has_key((key, section.name)): + # Yes - infinite loop detected + raise InterpolationLoopError(key) + # Place a marker on our backtrail so we won't come back here again + backtrail[(key, section.name)] = 1 + + # Now start the actual work + match = self._KEYCRE.search(value) + while match: + # The actual parsing of the match is implementation-dependent, + # so delegate to our helper function + k, v, s = self._parse_match(match) + if k is None: + # That's the signal that no further interpolation is needed + replacement = v + else: + # Further interpolation may be needed to obtain final value + replacement = recursive_interpolate(k, v, s, backtrail) + # Replace the matched string with its final value + start, end = match.span() + value = ''.join((value[:start], replacement, value[end:])) + new_search_start = start + len(replacement) + # Pick up the next interpolation key, if any, for next time + # through the while loop + match = self._KEYCRE.search(value, new_search_start) + + # Now safe to come back here again; remove marker from backtrail + del backtrail[(key, section.name)] + + return value + + # Back in interpolate(), all we have to do is kick off the recursive + # function with appropriate starting values + value = recursive_interpolate(key, value, self.section, {}) + return value + + + def _fetch(self, key): + """Helper function to fetch values from owning section. + + Returns a 2-tuple: the value, and the section where it was found. + """ + # switch off interpolation before we try and fetch anything ! + save_interp = self.section.main.interpolation + self.section.main.interpolation = False + + # Start at section that "owns" this InterpolationEngine + current_section = self.section + while True: + # try the current section first + val = current_section.get(key) + if val is not None: + break + # try "DEFAULT" next + val = current_section.get('DEFAULT', {}).get(key) + if val is not None: + break + # move up to parent and try again + # top-level's parent is itself + if current_section.parent is current_section: + # reached top level, time to give up + break + current_section = current_section.parent + + # restore interpolation to previous value before returning + self.section.main.interpolation = save_interp + if val is None: + raise MissingInterpolationOption(key) + return val, current_section + + + def _parse_match(self, match): + """Implementation-dependent helper function. + + Will be passed a match object corresponding to the interpolation + key we just found (e.g., "%(foo)s" or "$foo"). Should look up that + key in the appropriate config file section (using the ``_fetch()`` + helper function) and return a 3-tuple: (key, value, section) + + ``key`` is the name of the key we're looking for + ``value`` is the value found for that key + ``section`` is a reference to the section where it was found + + ``key`` and ``section`` should be None if no further + interpolation should be performed on the resulting value + (e.g., if we interpolated "$$" and returned "$"). + """ + raise NotImplementedError() + + + +class ConfigParserInterpolation(InterpolationEngine): + """Behaves like ConfigParser.""" + _KEYCRE = re.compile(r"%\(([^)]*)\)s") + + def _parse_match(self, match): + key = match.group(1) + value, section = self._fetch(key) + return key, value, section + + + +class TemplateInterpolation(InterpolationEngine): + """Behaves like string.Template.""" + _delimiter = '$' + _KEYCRE = re.compile(r""" + \$(?: + (?P\$) | # Two $ signs + (?P[_a-z][_a-z0-9]*) | # $name format + {(?P[^}]*)} # ${name} format + ) + """, re.IGNORECASE | re.VERBOSE) + + def _parse_match(self, match): + # Valid name (in or out of braces): fetch value from section + key = match.group('named') or match.group('braced') + if key is not None: + value, section = self._fetch(key) + return key, value, section + # Escaped delimiter (e.g., $$): return single delimiter + if match.group('escaped') is not None: + # Return None for key and section to indicate it's time to stop + return None, self._delimiter, None + # Anything else: ignore completely, just return it unchanged + return None, match.group(), None + + +interpolation_engines = { + 'configparser': ConfigParserInterpolation, + 'template': TemplateInterpolation, +} + + +def __newobj__(cls, *args): + # Hack for pickle + return cls.__new__(cls, *args) + +class Section(dict): + """ + A dictionary-like object that represents a section in a config file. + + It does string interpolation if the 'interpolation' attribute + of the 'main' object is set to True. + + Interpolation is tried first from this object, then from the 'DEFAULT' + section of this object, next from the parent and its 'DEFAULT' section, + and so on until the main object is reached. + + A Section will behave like an ordered dictionary - following the + order of the ``scalars`` and ``sections`` attributes. + You can use this to change the order of members. + + Iteration follows the order: scalars, then sections. + """ + + + def __setstate__(self, state): + dict.update(self, state[0]) + self.__dict__.update(state[1]) + + def __reduce__(self): + state = (dict(self), self.__dict__) + return (__newobj__, (self.__class__,), state) + + + def __init__(self, parent, depth, main, indict=None, name=None): + """ + * parent is the section above + * depth is the depth level of this section + * main is the main ConfigObj + * indict is a dictionary to initialise the section with + """ + if indict is None: + indict = {} + dict.__init__(self) + # used for nesting level *and* interpolation + self.parent = parent + # used for the interpolation attribute + self.main = main + # level of nesting depth of this Section + self.depth = depth + # purely for information + self.name = name + # + self._initialise() + # we do this explicitly so that __setitem__ is used properly + # (rather than just passing to ``dict.__init__``) + for entry, value in indict.iteritems(): + self[entry] = value + + + def _initialise(self): + # the sequence of scalar values in this Section + self.scalars = [] + # the sequence of sections in this Section + self.sections = [] + # for comments :-) + self.comments = {} + self.inline_comments = {} + # the configspec + self.configspec = None + # for defaults + self.defaults = [] + self.default_values = {} + + + def _interpolate(self, key, value): + try: + # do we already have an interpolation engine? + engine = self._interpolation_engine + except AttributeError: + # not yet: first time running _interpolate(), so pick the engine + name = self.main.interpolation + if name == True: # note that "if name:" would be incorrect here + # backwards-compatibility: interpolation=True means use default + name = DEFAULT_INTERPOLATION + name = name.lower() # so that "Template", "template", etc. all work + class_ = interpolation_engines.get(name, None) + if class_ is None: + # invalid value for self.main.interpolation + self.main.interpolation = False + return value + else: + # save reference to engine so we don't have to do this again + engine = self._interpolation_engine = class_(self) + # let the engine do the actual work + return engine.interpolate(key, value) + + + def __getitem__(self, key): + """Fetch the item and do string interpolation.""" + val = dict.__getitem__(self, key) + if self.main.interpolation and isinstance(val, basestring): + return self._interpolate(key, val) + return val + + + def __setitem__(self, key, value, unrepr=False): + """ + Correctly set a value. + + Making dictionary values Section instances. + (We have to special case 'Section' instances - which are also dicts) + + Keys must be strings. + Values need only be strings (or lists of strings) if + ``main.stringify`` is set. + + ``unrepr`` must be set when setting a value to a dictionary, without + creating a new sub-section. + """ + if not isinstance(key, basestring): + raise ValueError('The key "%s" is not a string.' % key) + + # add the comment + if not self.comments.has_key(key): + self.comments[key] = [] + self.inline_comments[key] = '' + # remove the entry from defaults + if key in self.defaults: + self.defaults.remove(key) + # + if isinstance(value, Section): + if not self.has_key(key): + self.sections.append(key) + dict.__setitem__(self, key, value) + elif isinstance(value, dict) and not unrepr: + # First create the new depth level, + # then create the section + if not self.has_key(key): + self.sections.append(key) + new_depth = self.depth + 1 + dict.__setitem__( + self, + key, + Section( + self, + new_depth, + self.main, + indict=value, + name=key)) + else: + if not self.has_key(key): + self.scalars.append(key) + if not self.main.stringify: + if isinstance(value, basestring): + pass + elif isinstance(value, (list, tuple)): + for entry in value: + if not isinstance(entry, basestring): + raise TypeError('Value is not a string "%s".' % entry) + else: + raise TypeError('Value is not a string "%s".' % value) + dict.__setitem__(self, key, value) + + + def __delitem__(self, key): + """Remove items from the sequence when deleting.""" + dict. __delitem__(self, key) + if key in self.scalars: + self.scalars.remove(key) + else: + self.sections.remove(key) + del self.comments[key] + del self.inline_comments[key] + + + def get(self, key, default=None): + """A version of ``get`` that doesn't bypass string interpolation.""" + try: + return self[key] + except KeyError: + return default + + + def update(self, indict): + """ + A version of update that uses our ``__setitem__``. + """ + for entry in indict: + self[entry] = indict[entry] + + + def pop(self, key, *args): + """ + 'D.pop(k[,d]) -> v, remove specified key and return the corresponding value. + If key is not found, d is returned if given, otherwise KeyError is raised' + """ + val = dict.pop(self, key, *args) + if key in self.scalars: + del self.comments[key] + del self.inline_comments[key] + self.scalars.remove(key) + elif key in self.sections: + del self.comments[key] + del self.inline_comments[key] + self.sections.remove(key) + if self.main.interpolation and isinstance(val, basestring): + return self._interpolate(key, val) + return val + + + def popitem(self): + """Pops the first (key,val)""" + sequence = (self.scalars + self.sections) + if not sequence: + raise KeyError(": 'popitem(): dictionary is empty'") + key = sequence[0] + val = self[key] + del self[key] + return key, val + + + def clear(self): + """ + A version of clear that also affects scalars/sections + Also clears comments and configspec. + + Leaves other attributes alone : + depth/main/parent are not affected + """ + dict.clear(self) + self.scalars = [] + self.sections = [] + self.comments = {} + self.inline_comments = {} + self.configspec = None + + + def setdefault(self, key, default=None): + """A version of setdefault that sets sequence if appropriate.""" + try: + return self[key] + except KeyError: + self[key] = default + return self[key] + + + def items(self): + """D.items() -> list of D's (key, value) pairs, as 2-tuples""" + return zip((self.scalars + self.sections), self.values()) + + + def keys(self): + """D.keys() -> list of D's keys""" + return (self.scalars + self.sections) + + + def values(self): + """D.values() -> list of D's values""" + return [self[key] for key in (self.scalars + self.sections)] + + + def iteritems(self): + """D.iteritems() -> an iterator over the (key, value) items of D""" + return iter(self.items()) + + + def iterkeys(self): + """D.iterkeys() -> an iterator over the keys of D""" + return iter((self.scalars + self.sections)) + + __iter__ = iterkeys + + + def itervalues(self): + """D.itervalues() -> an iterator over the values of D""" + return iter(self.values()) + + + def __repr__(self): + """x.__repr__() <==> repr(x)""" + return '{%s}' % ', '.join([('%s: %s' % (repr(key), repr(self[key]))) + for key in (self.scalars + self.sections)]) + + __str__ = __repr__ + __str__.__doc__ = "x.__str__() <==> str(x)" + + + # Extra methods - not in a normal dictionary + + def dict(self): + """ + Return a deepcopy of self as a dictionary. + + All members that are ``Section`` instances are recursively turned to + ordinary dictionaries - by calling their ``dict`` method. + + >>> n = a.dict() + >>> n == a + 1 + >>> n is a + 0 + """ + newdict = {} + for entry in self: + this_entry = self[entry] + if isinstance(this_entry, Section): + this_entry = this_entry.dict() + elif isinstance(this_entry, list): + # create a copy rather than a reference + this_entry = list(this_entry) + elif isinstance(this_entry, tuple): + # create a copy rather than a reference + this_entry = tuple(this_entry) + newdict[entry] = this_entry + return newdict + + + def merge(self, indict): + """ + A recursive update - useful for merging config files. + + >>> a = '''[section1] + ... option1 = True + ... [[subsection]] + ... more_options = False + ... # end of file'''.splitlines() + >>> b = '''# File is user.ini + ... [section1] + ... option1 = False + ... # end of file'''.splitlines() + >>> c1 = ConfigObj(b) + >>> c2 = ConfigObj(a) + >>> c2.merge(c1) + >>> c2 + ConfigObj({'section1': {'option1': 'False', 'subsection': {'more_options': 'False'}}}) + """ + for key, val in indict.items(): + if (key in self and isinstance(self[key], dict) and + isinstance(val, dict)): + self[key].merge(val) + else: + self[key] = val + + + def rename(self, oldkey, newkey): + """ + Change a keyname to another, without changing position in sequence. + + Implemented so that transformations can be made on keys, + as well as on values. (used by encode and decode) + + Also renames comments. + """ + if oldkey in self.scalars: + the_list = self.scalars + elif oldkey in self.sections: + the_list = self.sections + else: + raise KeyError('Key "%s" not found.' % oldkey) + pos = the_list.index(oldkey) + # + val = self[oldkey] + dict.__delitem__(self, oldkey) + dict.__setitem__(self, newkey, val) + the_list.remove(oldkey) + the_list.insert(pos, newkey) + comm = self.comments[oldkey] + inline_comment = self.inline_comments[oldkey] + del self.comments[oldkey] + del self.inline_comments[oldkey] + self.comments[newkey] = comm + self.inline_comments[newkey] = inline_comment + + + def walk(self, function, raise_errors=True, + call_on_sections=False, **keywargs): + """ + Walk every member and call a function on the keyword and value. + + Return a dictionary of the return values + + If the function raises an exception, raise the errror + unless ``raise_errors=False``, in which case set the return value to + ``False``. + + Any unrecognised keyword arguments you pass to walk, will be pased on + to the function you pass in. + + Note: if ``call_on_sections`` is ``True`` then - on encountering a + subsection, *first* the function is called for the *whole* subsection, + and then recurses into it's members. This means your function must be + able to handle strings, dictionaries and lists. This allows you + to change the key of subsections as well as for ordinary members. The + return value when called on the whole subsection has to be discarded. + + See the encode and decode methods for examples, including functions. + + .. admonition:: caution + + You can use ``walk`` to transform the names of members of a section + but you mustn't add or delete members. + + >>> config = '''[XXXXsection] + ... XXXXkey = XXXXvalue'''.splitlines() + >>> cfg = ConfigObj(config) + >>> cfg + ConfigObj({'XXXXsection': {'XXXXkey': 'XXXXvalue'}}) + >>> def transform(section, key): + ... val = section[key] + ... newkey = key.replace('XXXX', 'CLIENT1') + ... section.rename(key, newkey) + ... if isinstance(val, (tuple, list, dict)): + ... pass + ... else: + ... val = val.replace('XXXX', 'CLIENT1') + ... section[newkey] = val + >>> cfg.walk(transform, call_on_sections=True) + {'CLIENT1section': {'CLIENT1key': None}} + >>> cfg + ConfigObj({'CLIENT1section': {'CLIENT1key': 'CLIENT1value'}}) + """ + out = {} + # scalars first + for i in range(len(self.scalars)): + entry = self.scalars[i] + try: + val = function(self, entry, **keywargs) + # bound again in case name has changed + entry = self.scalars[i] + out[entry] = val + except Exception: + if raise_errors: + raise + else: + entry = self.scalars[i] + out[entry] = False + # then sections + for i in range(len(self.sections)): + entry = self.sections[i] + if call_on_sections: + try: + function(self, entry, **keywargs) + except Exception: + if raise_errors: + raise + else: + entry = self.sections[i] + out[entry] = False + # bound again in case name has changed + entry = self.sections[i] + # previous result is discarded + out[entry] = self[entry].walk( + function, + raise_errors=raise_errors, + call_on_sections=call_on_sections, + **keywargs) + return out + + + def as_bool(self, key): + """ + Accepts a key as input. The corresponding value must be a string or + the objects (``True`` or 1) or (``False`` or 0). We allow 0 and 1 to + retain compatibility with Python 2.2. + + If the string is one of ``True``, ``On``, ``Yes``, or ``1`` it returns + ``True``. + + If the string is one of ``False``, ``Off``, ``No``, or ``0`` it returns + ``False``. + + ``as_bool`` is not case sensitive. + + Any other input will raise a ``ValueError``. + + >>> a = ConfigObj() + >>> a['a'] = 'fish' + >>> a.as_bool('a') + Traceback (most recent call last): + ValueError: Value "fish" is neither True nor False + >>> a['b'] = 'True' + >>> a.as_bool('b') + 1 + >>> a['b'] = 'off' + >>> a.as_bool('b') + 0 + """ + val = self[key] + if val == True: + return True + elif val == False: + return False + else: + try: + if not isinstance(val, basestring): + # TODO: Why do we raise a KeyError here? + raise KeyError() + else: + return self.main._bools[val.lower()] + except KeyError: + raise ValueError('Value "%s" is neither True nor False' % val) + + + def as_int(self, key): + """ + A convenience method which coerces the specified value to an integer. + + If the value is an invalid literal for ``int``, a ``ValueError`` will + be raised. + + >>> a = ConfigObj() + >>> a['a'] = 'fish' + >>> a.as_int('a') + Traceback (most recent call last): + ValueError: invalid literal for int() with base 10: 'fish' + >>> a['b'] = '1' + >>> a.as_int('b') + 1 + >>> a['b'] = '3.2' + >>> a.as_int('b') + Traceback (most recent call last): + ValueError: invalid literal for int() with base 10: '3.2' + """ + return int(self[key]) + + + def as_float(self, key): + """ + A convenience method which coerces the specified value to a float. + + If the value is an invalid literal for ``float``, a ``ValueError`` will + be raised. + + >>> a = ConfigObj() + >>> a['a'] = 'fish' + >>> a.as_float('a') + Traceback (most recent call last): + ValueError: invalid literal for float(): fish + >>> a['b'] = '1' + >>> a.as_float('b') + 1.0 + >>> a['b'] = '3.2' + >>> a.as_float('b') + 3.2000000000000002 + """ + return float(self[key]) + + + def as_list(self, key): + """ + A convenience method which fetches the specified value, guaranteeing + that it is a list. + + >>> a = ConfigObj() + >>> a['a'] = 1 + >>> a.as_list('a') + [1] + >>> a['a'] = (1,) + >>> a.as_list('a') + [1] + >>> a['a'] = [1] + >>> a.as_list('a') + [1] + """ + result = self[key] + if isinstance(result, (tuple, list)): + return list(result) + return [result] + + + def restore_default(self, key): + """ + Restore (and return) default value for the specified key. + + This method will only work for a ConfigObj that was created + with a configspec and has been validated. + + If there is no default value for this key, ``KeyError`` is raised. + """ + default = self.default_values[key] + dict.__setitem__(self, key, default) + if key not in self.defaults: + self.defaults.append(key) + return default + + + def restore_defaults(self): + """ + Recursively restore default values to all members + that have them. + + This method will only work for a ConfigObj that was created + with a configspec and has been validated. + + It doesn't delete or modify entries without default values. + """ + for key in self.default_values: + self.restore_default(key) + + for section in self.sections: + self[section].restore_defaults() + + +class ConfigObj(Section): + """An object to read, create, and write config files.""" + + _keyword = re.compile(r'''^ # line start + (\s*) # indentation + ( # keyword + (?:".*?")| # double quotes + (?:'.*?')| # single quotes + (?:[^'"=].*?) # no quotes + ) + \s*=\s* # divider + (.*) # value (including list values and comments) + $ # line end + ''', + re.VERBOSE) + + _sectionmarker = re.compile(r'''^ + (\s*) # 1: indentation + ((?:\[\s*)+) # 2: section marker open + ( # 3: section name open + (?:"\s*\S.*?\s*")| # at least one non-space with double quotes + (?:'\s*\S.*?\s*')| # at least one non-space with single quotes + (?:[^'"\s].*?) # at least one non-space unquoted + ) # section name close + ((?:\s*\])+) # 4: section marker close + \s*(\#.*)? # 5: optional comment + $''', + re.VERBOSE) + + # this regexp pulls list values out as a single string + # or single values and comments + # FIXME: this regex adds a '' to the end of comma terminated lists + # workaround in ``_handle_value`` + _valueexp = re.compile(r'''^ + (?: + (?: + ( + (?: + (?: + (?:".*?")| # double quotes + (?:'.*?')| # single quotes + (?:[^'",\#][^,\#]*?) # unquoted + ) + \s*,\s* # comma + )* # match all list items ending in a comma (if any) + ) + ( + (?:".*?")| # double quotes + (?:'.*?')| # single quotes + (?:[^'",\#\s][^,]*?)| # unquoted + (?:(? 1: + msg = "Parsing failed with several errors.\nFirst error %s" % info + error = ConfigObjError(msg) + else: + error = self._errors[0] + # set the errors attribute; it's a list of tuples: + # (error_type, message, line_number) + error.errors = self._errors + # set the config attribute + error.config = self + raise error + # delete private attributes + del self._errors + + if configspec is None: + self.configspec = None + else: + self._handle_configspec(configspec) + + + def _initialise(self, options=None): + if options is None: + options = OPTION_DEFAULTS + + # initialise a few variables + self.filename = None + self._errors = [] + self.raise_errors = options['raise_errors'] + self.interpolation = options['interpolation'] + self.list_values = options['list_values'] + self.create_empty = options['create_empty'] + self.file_error = options['file_error'] + self.stringify = options['stringify'] + self.indent_type = options['indent_type'] + self.encoding = options['encoding'] + self.default_encoding = options['default_encoding'] + self.BOM = False + self.newlines = None + self.write_empty_values = options['write_empty_values'] + self.unrepr = options['unrepr'] + + self.initial_comment = [] + self.final_comment = [] + self.configspec = None + + if self._inspec: + self.list_values = False + + # Clear section attributes as well + Section._initialise(self) + + + def __repr__(self): + return ('ConfigObj({%s})' % + ', '.join([('%s: %s' % (repr(key), repr(self[key]))) + for key in (self.scalars + self.sections)])) + + + def _handle_bom(self, infile): + """ + Handle any BOM, and decode if necessary. + + If an encoding is specified, that *must* be used - but the BOM should + still be removed (and the BOM attribute set). + + (If the encoding is wrongly specified, then a BOM for an alternative + encoding won't be discovered or removed.) + + If an encoding is not specified, UTF8 or UTF16 BOM will be detected and + removed. The BOM attribute will be set. UTF16 will be decoded to + unicode. + + NOTE: This method must not be called with an empty ``infile``. + + Specifying the *wrong* encoding is likely to cause a + ``UnicodeDecodeError``. + + ``infile`` must always be returned as a list of lines, but may be + passed in as a single string. + """ + if ((self.encoding is not None) and + (self.encoding.lower() not in BOM_LIST)): + # No need to check for a BOM + # the encoding specified doesn't have one + # just decode + return self._decode(infile, self.encoding) + + if isinstance(infile, (list, tuple)): + line = infile[0] + else: + line = infile + if self.encoding is not None: + # encoding explicitly supplied + # And it could have an associated BOM + # TODO: if encoding is just UTF16 - we ought to check for both + # TODO: big endian and little endian versions. + enc = BOM_LIST[self.encoding.lower()] + if enc == 'utf_16': + # For UTF16 we try big endian and little endian + for BOM, (encoding, final_encoding) in BOMS.items(): + if not final_encoding: + # skip UTF8 + continue + if infile.startswith(BOM): + ### BOM discovered + ##self.BOM = True + # Don't need to remove BOM + return self._decode(infile, encoding) + + # If we get this far, will *probably* raise a DecodeError + # As it doesn't appear to start with a BOM + return self._decode(infile, self.encoding) + + # Must be UTF8 + BOM = BOM_SET[enc] + if not line.startswith(BOM): + return self._decode(infile, self.encoding) + + newline = line[len(BOM):] + + # BOM removed + if isinstance(infile, (list, tuple)): + infile[0] = newline + else: + infile = newline + self.BOM = True + return self._decode(infile, self.encoding) + + # No encoding specified - so we need to check for UTF8/UTF16 + for BOM, (encoding, final_encoding) in BOMS.items(): + if not line.startswith(BOM): + continue + else: + # BOM discovered + self.encoding = final_encoding + if not final_encoding: + self.BOM = True + # UTF8 + # remove BOM + newline = line[len(BOM):] + if isinstance(infile, (list, tuple)): + infile[0] = newline + else: + infile = newline + # UTF8 - don't decode + if isinstance(infile, basestring): + return infile.splitlines(True) + else: + return infile + # UTF16 - have to decode + return self._decode(infile, encoding) + + # No BOM discovered and no encoding specified, just return + if isinstance(infile, basestring): + # infile read from a file will be a single string + return infile.splitlines(True) + return infile + + + def _a_to_u(self, aString): + """Decode ASCII strings to unicode if a self.encoding is specified.""" + if self.encoding: + return aString.decode('ascii') + else: + return aString + + + def _decode(self, infile, encoding): + """ + Decode infile to unicode. Using the specified encoding. + + if is a string, it also needs converting to a list. + """ + if isinstance(infile, basestring): + # can't be unicode + # NOTE: Could raise a ``UnicodeDecodeError`` + return infile.decode(encoding).splitlines(True) + for i, line in enumerate(infile): + if not isinstance(line, unicode): + # NOTE: The isinstance test here handles mixed lists of unicode/string + # NOTE: But the decode will break on any non-string values + # NOTE: Or could raise a ``UnicodeDecodeError`` + infile[i] = line.decode(encoding) + return infile + + + def _decode_element(self, line): + """Decode element to unicode if necessary.""" + if not self.encoding: + return line + if isinstance(line, str) and self.default_encoding: + return line.decode(self.default_encoding) + return line + + + def _str(self, value): + """ + Used by ``stringify`` within validate, to turn non-string values + into strings. + """ + if not isinstance(value, basestring): + return str(value) + else: + return value + + + def _parse(self, infile): + """Actually parse the config file.""" + temp_list_values = self.list_values + if self.unrepr: + self.list_values = False + + comment_list = [] + done_start = False + this_section = self + maxline = len(infile) - 1 + cur_index = -1 + reset_comment = False + + while cur_index < maxline: + if reset_comment: + comment_list = [] + cur_index += 1 + line = infile[cur_index] + sline = line.strip() + # do we have anything on the line ? + if not sline or sline.startswith('#'): + reset_comment = False + comment_list.append(line) + continue + + if not done_start: + # preserve initial comment + self.initial_comment = comment_list + comment_list = [] + done_start = True + + reset_comment = True + # first we check if it's a section marker + mat = self._sectionmarker.match(line) + if mat is not None: + # is a section line + (indent, sect_open, sect_name, sect_close, comment) = mat.groups() + if indent and (self.indent_type is None): + self.indent_type = indent + cur_depth = sect_open.count('[') + if cur_depth != sect_close.count(']'): + self._handle_error("Cannot compute the section depth at line %s.", + NestingError, infile, cur_index) + continue + + if cur_depth < this_section.depth: + # the new section is dropping back to a previous level + try: + parent = self._match_depth(this_section, + cur_depth).parent + except SyntaxError: + self._handle_error("Cannot compute nesting level at line %s.", + NestingError, infile, cur_index) + continue + elif cur_depth == this_section.depth: + # the new section is a sibling of the current section + parent = this_section.parent + elif cur_depth == this_section.depth + 1: + # the new section is a child the current section + parent = this_section + else: + self._handle_error("Section too nested at line %s.", + NestingError, infile, cur_index) + + sect_name = self._unquote(sect_name) + if parent.has_key(sect_name): + self._handle_error('Duplicate section name at line %s.', + DuplicateError, infile, cur_index) + continue + + # create the new section + this_section = Section( + parent, + cur_depth, + self, + name=sect_name) + parent[sect_name] = this_section + parent.inline_comments[sect_name] = comment + parent.comments[sect_name] = comment_list + continue + # + # it's not a section marker, + # so it should be a valid ``key = value`` line + mat = self._keyword.match(line) + if mat is None: + # it neither matched as a keyword + # or a section marker + self._handle_error( + 'Invalid line at line "%s".', + ParseError, infile, cur_index) + else: + # is a keyword value + # value will include any inline comment + (indent, key, value) = mat.groups() + if indent and (self.indent_type is None): + self.indent_type = indent + # check for a multiline value + if value[:3] in ['"""', "'''"]: + try: + (value, comment, cur_index) = self._multiline( + value, infile, cur_index, maxline) + except SyntaxError: + self._handle_error( + 'Parse error in value at line %s.', + ParseError, infile, cur_index) + continue + else: + if self.unrepr: + comment = '' + try: + value = unrepr(value) + except Exception, e: + if type(e) == UnknownType: + msg = 'Unknown name or type in value at line %s.' + else: + msg = 'Parse error in value at line %s.' + self._handle_error(msg, UnreprError, infile, + cur_index) + continue + else: + if self.unrepr: + comment = '' + try: + value = unrepr(value) + except Exception, e: + if isinstance(e, UnknownType): + msg = 'Unknown name or type in value at line %s.' + else: + msg = 'Parse error in value at line %s.' + self._handle_error(msg, UnreprError, infile, + cur_index) + continue + else: + # extract comment and lists + try: + (value, comment) = self._handle_value(value) + except SyntaxError: + self._handle_error( + 'Parse error in value at line %s.', + ParseError, infile, cur_index) + continue + # + key = self._unquote(key) + if this_section.has_key(key): + self._handle_error( + 'Duplicate keyword name at line %s.', + DuplicateError, infile, cur_index) + continue + # add the key. + # we set unrepr because if we have got this far we will never + # be creating a new section + this_section.__setitem__(key, value, unrepr=True) + this_section.inline_comments[key] = comment + this_section.comments[key] = comment_list + continue + # + if self.indent_type is None: + # no indentation used, set the type accordingly + self.indent_type = '' + + # preserve the final comment + if not self and not self.initial_comment: + self.initial_comment = comment_list + elif not reset_comment: + self.final_comment = comment_list + self.list_values = temp_list_values + + + def _match_depth(self, sect, depth): + """ + Given a section and a depth level, walk back through the sections + parents to see if the depth level matches a previous section. + + Return a reference to the right section, + or raise a SyntaxError. + """ + while depth < sect.depth: + if sect is sect.parent: + # we've reached the top level already + raise SyntaxError() + sect = sect.parent + if sect.depth == depth: + return sect + # shouldn't get here + raise SyntaxError() + + + def _handle_error(self, text, ErrorClass, infile, cur_index): + """ + Handle an error according to the error settings. + + Either raise the error or store it. + The error will have occured at ``cur_index`` + """ + line = infile[cur_index] + cur_index += 1 + message = text % cur_index + error = ErrorClass(message, cur_index, line) + if self.raise_errors: + # raise the error - parsing stops here + raise error + # store the error + # reraise when parsing has finished + self._errors.append(error) + + + def _unquote(self, value): + """Return an unquoted version of a value""" + if (value[0] == value[-1]) and (value[0] in ('"', "'")): + value = value[1:-1] + return value + + + def _quote(self, value, multiline=True): + """ + Return a safely quoted version of a value. + + Raise a ConfigObjError if the value cannot be safely quoted. + If multiline is ``True`` (default) then use triple quotes + if necessary. + + * Don't quote values that don't need it. + * Recursively quote members of a list and return a comma joined list. + * Multiline is ``False`` for lists. + * Obey list syntax for empty and single member lists. + + If ``list_values=False`` then the value is only quoted if it contains + a ``\\n`` (is multiline) or '#'. + + If ``write_empty_values`` is set, and the value is an empty string, it + won't be quoted. + """ + if multiline and self.write_empty_values and value == '': + # Only if multiline is set, so that it is used for values not + # keys, and not values that are part of a list + return '' + + if multiline and isinstance(value, (list, tuple)): + if not value: + return ',' + elif len(value) == 1: + return self._quote(value[0], multiline=False) + ',' + return ', '.join([self._quote(val, multiline=False) + for val in value]) + if not isinstance(value, basestring): + if self.stringify: + value = str(value) + else: + raise TypeError('Value "%s" is not a string.' % value) + + if not value: + return '""' + + no_lists_no_quotes = not self.list_values and '\n' not in value and '#' not in value + need_triple = multiline and ((("'" in value) and ('"' in value)) or ('\n' in value )) + hash_triple_quote = multiline and not need_triple and ("'" in value) and ('"' in value) and ('#' in value) + check_for_single = (no_lists_no_quotes or not need_triple) and not hash_triple_quote + + if check_for_single: + if not self.list_values: + # we don't quote if ``list_values=False`` + quot = noquot + # for normal values either single or double quotes will do + elif '\n' in value: + # will only happen if multiline is off - e.g. '\n' in key + raise ConfigObjError('Value "%s" cannot be safely quoted.' % value) + elif ((value[0] not in wspace_plus) and + (value[-1] not in wspace_plus) and + (',' not in value)): + quot = noquot + else: + quot = self._get_single_quote(value) + else: + # if value has '\n' or "'" *and* '"', it will need triple quotes + quot = self._get_triple_quote(value) + + if quot == noquot and '#' in value and self.list_values: + quot = self._get_single_quote(value) + + return quot % value + + + def _get_single_quote(self, value): + if ("'" in value) and ('"' in value): + raise ConfigObjError('Value "%s" cannot be safely quoted.' % value) + elif '"' in value: + quot = squot + else: + quot = dquot + return quot + + + def _get_triple_quote(self, value): + if (value.find('"""') != -1) and (value.find("'''") != -1): + raise ConfigObjError('Value "%s" cannot be safely quoted.' % value) + if value.find('"""') == -1: + quot = tdquot + else: + quot = tsquot + return quot + + + def _handle_value(self, value): + """ + Given a value string, unquote, remove comment, + handle lists. (including empty and single member lists) + """ + if self._inspec: + # Parsing a configspec so don't handle comments + return (value, '') + # do we look for lists in values ? + if not self.list_values: + mat = self._nolistvalue.match(value) + if mat is None: + raise SyntaxError() + # NOTE: we don't unquote here + return mat.groups() + # + mat = self._valueexp.match(value) + if mat is None: + # the value is badly constructed, probably badly quoted, + # or an invalid list + raise SyntaxError() + (list_values, single, empty_list, comment) = mat.groups() + if (list_values == '') and (single is None): + # change this if you want to accept empty values + raise SyntaxError() + # NOTE: note there is no error handling from here if the regex + # is wrong: then incorrect values will slip through + if empty_list is not None: + # the single comma - meaning an empty list + return ([], comment) + if single is not None: + # handle empty values + if list_values and not single: + # FIXME: the '' is a workaround because our regex now matches + # '' at the end of a list if it has a trailing comma + single = None + else: + single = single or '""' + single = self._unquote(single) + if list_values == '': + # not a list value + return (single, comment) + the_list = self._listvalueexp.findall(list_values) + the_list = [self._unquote(val) for val in the_list] + if single is not None: + the_list += [single] + return (the_list, comment) + + + def _multiline(self, value, infile, cur_index, maxline): + """Extract the value, where we are in a multiline situation.""" + quot = value[:3] + newvalue = value[3:] + single_line = self._triple_quote[quot][0] + multi_line = self._triple_quote[quot][1] + mat = single_line.match(value) + if mat is not None: + retval = list(mat.groups()) + retval.append(cur_index) + return retval + elif newvalue.find(quot) != -1: + # somehow the triple quote is missing + raise SyntaxError() + # + while cur_index < maxline: + cur_index += 1 + newvalue += '\n' + line = infile[cur_index] + if line.find(quot) == -1: + newvalue += line + else: + # end of multiline, process it + break + else: + # we've got to the end of the config, oops... + raise SyntaxError() + mat = multi_line.match(line) + if mat is None: + # a badly formed line + raise SyntaxError() + (value, comment) = mat.groups() + return (newvalue + value, comment, cur_index) + + + def _handle_configspec(self, configspec): + """Parse the configspec.""" + # FIXME: Should we check that the configspec was created with the + # correct settings ? (i.e. ``list_values=False``) + if not isinstance(configspec, ConfigObj): + try: + configspec = ConfigObj(configspec, + raise_errors=True, + file_error=True, + _inspec=True) + except ConfigObjError, e: + # FIXME: Should these errors have a reference + # to the already parsed ConfigObj ? + raise ConfigspecError('Parsing configspec failed: %s' % e) + except IOError, e: + raise IOError('Reading configspec failed: %s' % e) + + self.configspec = configspec + + + + def _set_configspec(self, section, copy): + """ + Called by validate. Handles setting the configspec on subsections + including sections to be validated by __many__ + """ + configspec = section.configspec + many = configspec.get('__many__') + if isinstance(many, dict): + for entry in section.sections: + if entry not in configspec: + section[entry].configspec = many + + for entry in configspec.sections: + if entry == '__many__': + continue + if entry not in section: + section[entry] = {} + if copy: + # copy comments + section.comments[entry] = configspec.comments.get(entry, []) + section.inline_comments[entry] = configspec.inline_comments.get(entry, '') + + # Could be a scalar when we expect a section + if isinstance(section[entry], Section): + section[entry].configspec = configspec[entry] + + + def _write_line(self, indent_string, entry, this_entry, comment): + """Write an individual line, for the write method""" + # NOTE: the calls to self._quote here handles non-StringType values. + if not self.unrepr: + val = self._decode_element(self._quote(this_entry)) + else: + val = repr(this_entry) + return '%s%s%s%s%s' % (indent_string, + self._decode_element(self._quote(entry, multiline=False)), + self._a_to_u(' = '), + val, + self._decode_element(comment)) + + + def _write_marker(self, indent_string, depth, entry, comment): + """Write a section marker line""" + return '%s%s%s%s%s' % (indent_string, + self._a_to_u('[' * depth), + self._quote(self._decode_element(entry), multiline=False), + self._a_to_u(']' * depth), + self._decode_element(comment)) + + + def _handle_comment(self, comment): + """Deal with a comment.""" + if not comment: + return '' + start = self.indent_type + if not comment.startswith('#'): + start += self._a_to_u(' # ') + return (start + comment) + + + # Public methods + + def write(self, outfile=None, section=None): + """ + Write the current ConfigObj as a file + + tekNico: FIXME: use StringIO instead of real files + + >>> filename = a.filename + >>> a.filename = 'test.ini' + >>> a.write() + >>> a.filename = filename + >>> a == ConfigObj('test.ini', raise_errors=True) + 1 + """ + if self.indent_type is None: + # this can be true if initialised from a dictionary + self.indent_type = DEFAULT_INDENT_TYPE + + out = [] + cs = self._a_to_u('#') + csp = self._a_to_u('# ') + if section is None: + int_val = self.interpolation + self.interpolation = False + section = self + for line in self.initial_comment: + line = self._decode_element(line) + stripped_line = line.strip() + if stripped_line and not stripped_line.startswith(cs): + line = csp + line + out.append(line) + + indent_string = self.indent_type * section.depth + for entry in (section.scalars + section.sections): + if entry in section.defaults: + # don't write out default values + continue + for comment_line in section.comments[entry]: + comment_line = self._decode_element(comment_line.lstrip()) + if comment_line and not comment_line.startswith(cs): + comment_line = csp + comment_line + out.append(indent_string + comment_line) + this_entry = section[entry] + comment = self._handle_comment(section.inline_comments[entry]) + + if isinstance(this_entry, dict): + # a section + out.append(self._write_marker( + indent_string, + this_entry.depth, + entry, + comment)) + out.extend(self.write(section=this_entry)) + else: + out.append(self._write_line( + indent_string, + entry, + this_entry, + comment)) + + if section is self: + for line in self.final_comment: + line = self._decode_element(line) + stripped_line = line.strip() + if stripped_line and not stripped_line.startswith(cs): + line = csp + line + out.append(line) + self.interpolation = int_val + + if section is not self: + return out + + if (self.filename is None) and (outfile is None): + # output a list of lines + # might need to encode + # NOTE: This will *screw* UTF16, each line will start with the BOM + if self.encoding: + out = [l.encode(self.encoding) for l in out] + if (self.BOM and ((self.encoding is None) or + (BOM_LIST.get(self.encoding.lower()) == 'utf_8'))): + # Add the UTF8 BOM + if not out: + out.append('') + out[0] = BOM_UTF8 + out[0] + return out + + # Turn the list to a string, joined with correct newlines + newline = self.newlines or os.linesep + output = self._a_to_u(newline).join(out) + if self.encoding: + output = output.encode(self.encoding) + if self.BOM and ((self.encoding is None) or match_utf8(self.encoding)): + # Add the UTF8 BOM + output = BOM_UTF8 + output + + if not output.endswith(newline): + output += newline + if outfile is not None: + outfile.write(output) + else: + h = open(self.filename, 'wb') + h.write(output) + h.close() + + + def validate(self, validator, preserve_errors=False, copy=False, + section=None): + """ + Test the ConfigObj against a configspec. + + It uses the ``validator`` object from *validate.py*. + + To run ``validate`` on the current ConfigObj, call: :: + + test = config.validate(validator) + + (Normally having previously passed in the configspec when the ConfigObj + was created - you can dynamically assign a dictionary of checks to the + ``configspec`` attribute of a section though). + + It returns ``True`` if everything passes, or a dictionary of + pass/fails (True/False). If every member of a subsection passes, it + will just have the value ``True``. (It also returns ``False`` if all + members fail). + + In addition, it converts the values from strings to their native + types if their checks pass (and ``stringify`` is set). + + If ``preserve_errors`` is ``True`` (``False`` is default) then instead + of a marking a fail with a ``False``, it will preserve the actual + exception object. This can contain info about the reason for failure. + For example the ``VdtValueTooSmallError`` indicates that the value + supplied was too small. If a value (or section) is missing it will + still be marked as ``False``. + + You must have the validate module to use ``preserve_errors=True``. + + You can then use the ``flatten_errors`` function to turn your nested + results dictionary into a flattened list of failures - useful for + displaying meaningful error messages. + """ + if section is None: + if self.configspec is None: + raise ValueError('No configspec supplied.') + if preserve_errors: + # We do this once to remove a top level dependency on the validate module + # Which makes importing configobj faster + from validate import VdtMissingValue + self._vdtMissingValue = VdtMissingValue + + section = self + + if copy: + section.initial_comment = section.configspec.initial_comment + section.final_comment = section.configspec.final_comment + section.encoding = section.configspec.encoding + section.BOM = section.configspec.BOM + section.newlines = section.configspec.newlines + section.indent_type = section.configspec.indent_type + + # + configspec = section.configspec + self._set_configspec(section, copy) + + def validate_entry(entry, spec, val, missing, ret_true, ret_false): + try: + check = validator.check(spec, + val, + missing=missing + ) + except validator.baseErrorClass, e: + if not preserve_errors or isinstance(e, self._vdtMissingValue): + out[entry] = False + else: + # preserve the error + out[entry] = e + ret_false = False + ret_true = False + else: + try: + section.default_values.pop(entry, None) + except AttributeError: + # For Python 2.2 compatibility + try: + del section.default_values[entry] + except KeyError: + pass + + try: + section.default_values[entry] = validator.get_default_value(configspec[entry]) + except (KeyError, AttributeError): + # No default or validator has no 'get_default_value' (e.g. SimpleVal) + pass + + ret_false = False + out[entry] = True + if self.stringify or missing: + # if we are doing type conversion + # or the value is a supplied default + if not self.stringify: + if isinstance(check, (list, tuple)): + # preserve lists + check = [self._str(item) for item in check] + elif missing and check is None: + # convert the None from a default to a '' + check = '' + else: + check = self._str(check) + if (check != val) or missing: + section[entry] = check + if not copy and missing and entry not in section.defaults: + section.defaults.append(entry) + return ret_true, ret_false + + # + out = {} + ret_true = True + ret_false = True + + unvalidated = [k for k in section.scalars if k not in configspec] + incorrect_sections = [k for k in configspec.sections if k in section.scalars] + incorrect_scalars = [k for k in configspec.scalars if k in section.sections] + + for entry in configspec.scalars: + if entry in ('__many__', '___many___'): + # reserved names + continue + + if (not entry in section.scalars) or (entry in section.defaults): + # missing entries + # or entries from defaults + missing = True + val = None + if copy and not entry in section.scalars: + # copy comments + section.comments[entry] = ( + configspec.comments.get(entry, [])) + section.inline_comments[entry] = ( + configspec.inline_comments.get(entry, '')) + # + else: + missing = False + val = section[entry] + + ret_true, ret_false = validate_entry(entry, configspec[entry], val, + missing, ret_true, ret_false) + + many = None + if '__many__' in configspec.scalars: + many = configspec['__many__'] + elif '___many___' in configspec.scalars: + many = configspec['___many___'] + + if many is not None: + for entry in unvalidated: + val = section[entry] + ret_true, ret_false = validate_entry(entry, many, val, False, + ret_true, ret_false) + + for entry in incorrect_scalars: + ret_true = False + if not preserve_errors: + out[entry] = False + else: + ret_false = False + msg = 'Value %r was provided as a section' % entry + out[entry] = validator.baseErrorClass(msg) + for entry in incorrect_sections: + ret_true = False + if not preserve_errors: + out[entry] = False + else: + ret_false = False + msg = 'Section %r was provided as a single value' % entry + out[entry] = validator.baseErrorClass(msg) + + # Missing sections will have been created as empty ones when the + # configspec was read. + for entry in section.sections: + # FIXME: this means DEFAULT is not copied in copy mode + if section is self and entry == 'DEFAULT': + continue + if section[entry].configspec is None: + continue + if copy: + section.comments[entry] = configspec.comments.get(entry, []) + section.inline_comments[entry] = configspec.inline_comments.get(entry, '') + check = self.validate(validator, preserve_errors=preserve_errors, copy=copy, section=section[entry]) + out[entry] = check + if check == False: + ret_true = False + elif check == True: + ret_false = False + else: + ret_true = False + ret_false = False + # + if ret_true: + return True + elif ret_false: + return False + return out + + + def reset(self): + """Clear ConfigObj instance and restore to 'freshly created' state.""" + self.clear() + self._initialise() + # FIXME: Should be done by '_initialise', but ConfigObj constructor (and reload) + # requires an empty dictionary + self.configspec = None + # Just to be sure ;-) + self._original_configspec = None + + + def reload(self): + """ + Reload a ConfigObj from file. + + This method raises a ``ReloadError`` if the ConfigObj doesn't have + a filename attribute pointing to a file. + """ + if not isinstance(self.filename, basestring): + raise ReloadError() + + filename = self.filename + current_options = {} + for entry in OPTION_DEFAULTS: + if entry == 'configspec': + continue + current_options[entry] = getattr(self, entry) + + configspec = self._original_configspec + current_options['configspec'] = configspec + + self.clear() + self._initialise(current_options) + self._load(filename, configspec) + + + +class SimpleVal(object): + """ + A simple validator. + Can be used to check that all members expected are present. + + To use it, provide a configspec with all your members in (the value given + will be ignored). Pass an instance of ``SimpleVal`` to the ``validate`` + method of your ``ConfigObj``. ``validate`` will return ``True`` if all + members are present, or a dictionary with True/False meaning + present/missing. (Whole missing sections will be replaced with ``False``) + """ + + def __init__(self): + self.baseErrorClass = ConfigObjError + + def check(self, check, member, missing=False): + """A dummy check method, always returns the value unchanged.""" + if missing: + raise self.baseErrorClass() + return member + + +# Check / processing functions for options +def flatten_errors(cfg, res, levels=None, results=None): + """ + An example function that will turn a nested dictionary of results + (as returned by ``ConfigObj.validate``) into a flat list. + + ``cfg`` is the ConfigObj instance being checked, ``res`` is the results + dictionary returned by ``validate``. + + (This is a recursive function, so you shouldn't use the ``levels`` or + ``results`` arguments - they are used by the function.) + + Returns a list of keys that failed. Each member of the list is a tuple : + + :: + + ([list of sections...], key, result) + + If ``validate`` was called with ``preserve_errors=False`` (the default) + then ``result`` will always be ``False``. + + *list of sections* is a flattened list of sections that the key was found + in. + + If the section was missing (or a section was expected and a scalar provided + - or vice-versa) then key will be ``None``. + + If the value (or section) was missing then ``result`` will be ``False``. + + If ``validate`` was called with ``preserve_errors=True`` and a value + was present, but failed the check, then ``result`` will be the exception + object returned. You can use this as a string that describes the failure. + + For example *The value "3" is of the wrong type*. + + >>> import validate + >>> vtor = validate.Validator() + >>> my_ini = ''' + ... option1 = True + ... [section1] + ... option1 = True + ... [section2] + ... another_option = Probably + ... [section3] + ... another_option = True + ... [[section3b]] + ... value = 3 + ... value2 = a + ... value3 = 11 + ... ''' + >>> my_cfg = ''' + ... option1 = boolean() + ... option2 = boolean() + ... option3 = boolean(default=Bad_value) + ... [section1] + ... option1 = boolean() + ... option2 = boolean() + ... option3 = boolean(default=Bad_value) + ... [section2] + ... another_option = boolean() + ... [section3] + ... another_option = boolean() + ... [[section3b]] + ... value = integer + ... value2 = integer + ... value3 = integer(0, 10) + ... [[[section3b-sub]]] + ... value = string + ... [section4] + ... another_option = boolean() + ... ''' + >>> cs = my_cfg.split('\\n') + >>> ini = my_ini.split('\\n') + >>> cfg = ConfigObj(ini, configspec=cs) + >>> res = cfg.validate(vtor, preserve_errors=True) + >>> errors = [] + >>> for entry in flatten_errors(cfg, res): + ... section_list, key, error = entry + ... section_list.insert(0, '[root]') + ... if key is not None: + ... section_list.append(key) + ... else: + ... section_list.append('[missing]') + ... section_string = ', '.join(section_list) + ... errors.append((section_string, ' = ', error)) + >>> errors.sort() + >>> for entry in errors: + ... print entry[0], entry[1], (entry[2] or 0) + [root], option2 = 0 + [root], option3 = the value "Bad_value" is of the wrong type. + [root], section1, option2 = 0 + [root], section1, option3 = the value "Bad_value" is of the wrong type. + [root], section2, another_option = the value "Probably" is of the wrong type. + [root], section3, section3b, section3b-sub, [missing] = 0 + [root], section3, section3b, value2 = the value "a" is of the wrong type. + [root], section3, section3b, value3 = the value "11" is too big. + [root], section4, [missing] = 0 + """ + if levels is None: + # first time called + levels = [] + results = [] + if res is True: + return results + if res is False or isinstance(res, Exception): + results.append((levels[:], None, res)) + if levels: + levels.pop() + return results + for (key, val) in res.items(): + if val == True: + continue + if isinstance(cfg.get(key), dict): + # Go down one level + levels.append(key) + flatten_errors(cfg[key], val, levels, results) + continue + results.append((levels[:], key, val)) + # + # Go up one level + if levels: + levels.pop() + # + return results + + +"""*A programming language is a medium of expression.* - Paul Graham""" diff --git a/terminatorlib/configobj/validate.py b/terminatorlib/configobj/validate.py new file mode 100644 index 00000000..30bdfacb --- /dev/null +++ b/terminatorlib/configobj/validate.py @@ -0,0 +1,1465 @@ +# validate.py +# A Validator object +# Copyright (C) 2005 Michael Foord, Mark Andrews, Nicola Larosa +# E-mail: fuzzyman AT voidspace DOT org DOT uk +# mark AT la-la DOT com +# nico AT tekNico DOT net + +# This software is licensed under the terms of the BSD license. +# http://www.voidspace.org.uk/python/license.shtml +# Basically you're free to copy, modify, distribute and relicense it, +# So long as you keep a copy of the license with it. + +# Scripts maintained at http://www.voidspace.org.uk/python/index.shtml +# For information about bugfixes, updates and support, please join the +# ConfigObj mailing list: +# http://lists.sourceforge.net/lists/listinfo/configobj-develop +# Comments, suggestions and bug reports welcome. + +""" + The Validator object is used to check that supplied values + conform to a specification. + + The value can be supplied as a string - e.g. from a config file. + In this case the check will also *convert* the value to + the required type. This allows you to add validation + as a transparent layer to access data stored as strings. + The validation checks that the data is correct *and* + converts it to the expected type. + + Some standard checks are provided for basic data types. + Additional checks are easy to write. They can be + provided when the ``Validator`` is instantiated or + added afterwards. + + The standard functions work with the following basic data types : + + * integers + * floats + * booleans + * strings + * ip_addr + + plus lists of these datatypes + + Adding additional checks is done through coding simple functions. + + The full set of standard checks are : + + * 'integer': matches integer values (including negative) + Takes optional 'min' and 'max' arguments : :: + + integer() + integer(3, 9) # any value from 3 to 9 + integer(min=0) # any positive value + integer(max=9) + + * 'float': matches float values + Has the same parameters as the integer check. + + * 'boolean': matches boolean values - ``True`` or ``False`` + Acceptable string values for True are : + true, on, yes, 1 + Acceptable string values for False are : + false, off, no, 0 + + Any other value raises an error. + + * 'ip_addr': matches an Internet Protocol address, v.4, represented + by a dotted-quad string, i.e. '1.2.3.4'. + + * 'string': matches any string. + Takes optional keyword args 'min' and 'max' + to specify min and max lengths of the string. + + * 'list': matches any list. + Takes optional keyword args 'min', and 'max' to specify min and + max sizes of the list. (Always returns a list.) + + * 'tuple': matches any tuple. + Takes optional keyword args 'min', and 'max' to specify min and + max sizes of the tuple. (Always returns a tuple.) + + * 'int_list': Matches a list of integers. + Takes the same arguments as list. + + * 'float_list': Matches a list of floats. + Takes the same arguments as list. + + * 'bool_list': Matches a list of boolean values. + Takes the same arguments as list. + + * 'ip_addr_list': Matches a list of IP addresses. + Takes the same arguments as list. + + * 'string_list': Matches a list of strings. + Takes the same arguments as list. + + * 'mixed_list': Matches a list with different types in + specific positions. List size must match + the number of arguments. + + Each position can be one of : + 'integer', 'float', 'ip_addr', 'string', 'boolean' + + So to specify a list with two strings followed + by two integers, you write the check as : :: + + mixed_list('string', 'string', 'integer', 'integer') + + * 'pass': This check matches everything ! It never fails + and the value is unchanged. + + It is also the default if no check is specified. + + * 'option': This check matches any from a list of options. + You specify this check with : :: + + option('option 1', 'option 2', 'option 3') + + You can supply a default value (returned if no value is supplied) + using the default keyword argument. + + You specify a list argument for default using a list constructor syntax in + the check : :: + + checkname(arg1, arg2, default=list('val 1', 'val 2', 'val 3')) + + A badly formatted set of arguments will raise a ``VdtParamError``. +""" + +__docformat__ = "restructuredtext en" + +__version__ = '1.0.0' + +__revision__ = '$Id: validate.py 123 2005-09-08 08:54:28Z fuzzyman $' + +__all__ = ( + '__version__', + 'dottedQuadToNum', + 'numToDottedQuad', + 'ValidateError', + 'VdtUnknownCheckError', + 'VdtParamError', + 'VdtTypeError', + 'VdtValueError', + 'VdtValueTooSmallError', + 'VdtValueTooBigError', + 'VdtValueTooShortError', + 'VdtValueTooLongError', + 'VdtMissingValue', + 'Validator', + 'is_integer', + 'is_float', + 'is_boolean', + 'is_list', + 'is_tuple', + 'is_ip_addr', + 'is_string', + 'is_int_list', + 'is_bool_list', + 'is_float_list', + 'is_string_list', + 'is_ip_addr_list', + 'is_mixed_list', + 'is_option', + '__docformat__', +) + + +import re + + +_list_arg = re.compile(r''' + (?: + ([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*list\( + ( + (?: + \s* + (?: + (?:".*?")| # double quotes + (?:'.*?')| # single quotes + (?:[^'",\s\)][^,\)]*?) # unquoted + ) + \s*,\s* + )* + (?: + (?:".*?")| # double quotes + (?:'.*?')| # single quotes + (?:[^'",\s\)][^,\)]*?) # unquoted + )? # last one + ) + \) + ) +''', re.VERBOSE | re.DOTALL) # two groups + +_list_members = re.compile(r''' + ( + (?:".*?")| # double quotes + (?:'.*?')| # single quotes + (?:[^'",\s=][^,=]*?) # unquoted + ) + (?: + (?:\s*,\s*)|(?:\s*$) # comma + ) +''', re.VERBOSE | re.DOTALL) # one group + +_paramstring = r''' + (?: + ( + (?: + [a-zA-Z_][a-zA-Z0-9_]*\s*=\s*list\( + (?: + \s* + (?: + (?:".*?")| # double quotes + (?:'.*?')| # single quotes + (?:[^'",\s\)][^,\)]*?) # unquoted + ) + \s*,\s* + )* + (?: + (?:".*?")| # double quotes + (?:'.*?')| # single quotes + (?:[^'",\s\)][^,\)]*?) # unquoted + )? # last one + \) + )| + (?: + (?:".*?")| # double quotes + (?:'.*?')| # single quotes + (?:[^'",\s=][^,=]*?)| # unquoted + (?: # keyword argument + [a-zA-Z_][a-zA-Z0-9_]*\s*=\s* + (?: + (?:".*?")| # double quotes + (?:'.*?')| # single quotes + (?:[^'",\s=][^,=]*?) # unquoted + ) + ) + ) + ) + (?: + (?:\s*,\s*)|(?:\s*$) # comma + ) + ) + ''' + +_matchstring = '^%s*' % _paramstring + +# Python pre 2.2.1 doesn't have bool +try: + bool +except NameError: + def bool(val): + """Simple boolean equivalent function. """ + if val: + return 1 + else: + return 0 + + +def dottedQuadToNum(ip): + """ + Convert decimal dotted quad string to long integer + + >>> int(dottedQuadToNum('1 ')) + 1 + >>> int(dottedQuadToNum(' 1.2')) + 16777218 + >>> int(dottedQuadToNum(' 1.2.3 ')) + 16908291 + >>> int(dottedQuadToNum('1.2.3.4')) + 16909060 + >>> dottedQuadToNum('1.2.3. 4') + 16909060 + >>> dottedQuadToNum('255.255.255.255') + 4294967295L + >>> dottedQuadToNum('255.255.255.256') + Traceback (most recent call last): + ValueError: Not a good dotted-quad IP: 255.255.255.256 + """ + + # import here to avoid it when ip_addr values are not used + import socket, struct + + try: + return struct.unpack('!L', + socket.inet_aton(ip.strip()))[0] + except socket.error: + # bug in inet_aton, corrected in Python 2.3 + if ip.strip() == '255.255.255.255': + return 0xFFFFFFFFL + else: + raise ValueError('Not a good dotted-quad IP: %s' % ip) + return + + +def numToDottedQuad(num): + """ + Convert long int to dotted quad string + + >>> numToDottedQuad(-1L) + Traceback (most recent call last): + ValueError: Not a good numeric IP: -1 + >>> numToDottedQuad(1L) + '0.0.0.1' + >>> numToDottedQuad(16777218L) + '1.0.0.2' + >>> numToDottedQuad(16908291L) + '1.2.0.3' + >>> numToDottedQuad(16909060L) + '1.2.3.4' + >>> numToDottedQuad(4294967295L) + '255.255.255.255' + >>> numToDottedQuad(4294967296L) + Traceback (most recent call last): + ValueError: Not a good numeric IP: 4294967296 + """ + + # import here to avoid it when ip_addr values are not used + import socket, struct + + # no need to intercept here, 4294967295L is fine + if num > 4294967295L or num < 0: + raise ValueError('Not a good numeric IP: %s' % num) + try: + return socket.inet_ntoa( + struct.pack('!L', long(num))) + except (socket.error, struct.error, OverflowError): + raise ValueError('Not a good numeric IP: %s' % num) + + +class ValidateError(Exception): + """ + This error indicates that the check failed. + It can be the base class for more specific errors. + + Any check function that fails ought to raise this error. + (or a subclass) + + >>> raise ValidateError + Traceback (most recent call last): + ValidateError + """ + + +class VdtMissingValue(ValidateError): + """No value was supplied to a check that needed one.""" + + +class VdtUnknownCheckError(ValidateError): + """An unknown check function was requested""" + + def __init__(self, value): + """ + >>> raise VdtUnknownCheckError('yoda') + Traceback (most recent call last): + VdtUnknownCheckError: the check "yoda" is unknown. + """ + ValidateError.__init__(self, 'the check "%s" is unknown.' % (value,)) + + +class VdtParamError(SyntaxError): + """An incorrect parameter was passed""" + + def __init__(self, name, value): + """ + >>> raise VdtParamError('yoda', 'jedi') + Traceback (most recent call last): + VdtParamError: passed an incorrect value "jedi" for parameter "yoda". + """ + SyntaxError.__init__(self, 'passed an incorrect value "%s" for parameter "%s".' % (value, name)) + + +class VdtTypeError(ValidateError): + """The value supplied was of the wrong type""" + + def __init__(self, value): + """ + >>> raise VdtTypeError('jedi') + Traceback (most recent call last): + VdtTypeError: the value "jedi" is of the wrong type. + """ + ValidateError.__init__(self, 'the value "%s" is of the wrong type.' % (value,)) + + +class VdtValueError(ValidateError): + """The value supplied was of the correct type, but was not an allowed value.""" + + def __init__(self, value): + """ + >>> raise VdtValueError('jedi') + Traceback (most recent call last): + VdtValueError: the value "jedi" is unacceptable. + """ + ValidateError.__init__(self, 'the value "%s" is unacceptable.' % (value,)) + + +class VdtValueTooSmallError(VdtValueError): + """The value supplied was of the correct type, but was too small.""" + + def __init__(self, value): + """ + >>> raise VdtValueTooSmallError('0') + Traceback (most recent call last): + VdtValueTooSmallError: the value "0" is too small. + """ + ValidateError.__init__(self, 'the value "%s" is too small.' % (value,)) + + +class VdtValueTooBigError(VdtValueError): + """The value supplied was of the correct type, but was too big.""" + + def __init__(self, value): + """ + >>> raise VdtValueTooBigError('1') + Traceback (most recent call last): + VdtValueTooBigError: the value "1" is too big. + """ + ValidateError.__init__(self, 'the value "%s" is too big.' % (value,)) + + +class VdtValueTooShortError(VdtValueError): + """The value supplied was of the correct type, but was too short.""" + + def __init__(self, value): + """ + >>> raise VdtValueTooShortError('jed') + Traceback (most recent call last): + VdtValueTooShortError: the value "jed" is too short. + """ + ValidateError.__init__( + self, + 'the value "%s" is too short.' % (value,)) + + +class VdtValueTooLongError(VdtValueError): + """The value supplied was of the correct type, but was too long.""" + + def __init__(self, value): + """ + >>> raise VdtValueTooLongError('jedie') + Traceback (most recent call last): + VdtValueTooLongError: the value "jedie" is too long. + """ + ValidateError.__init__(self, 'the value "%s" is too long.' % (value,)) + + +class Validator(object): + """ + Validator is an object that allows you to register a set of 'checks'. + These checks take input and test that it conforms to the check. + + This can also involve converting the value from a string into + the correct datatype. + + The ``check`` method takes an input string which configures which + check is to be used and applies that check to a supplied value. + + An example input string would be: + 'int_range(param1, param2)' + + You would then provide something like: + + >>> def int_range_check(value, min, max): + ... # turn min and max from strings to integers + ... min = int(min) + ... max = int(max) + ... # check that value is of the correct type. + ... # possible valid inputs are integers or strings + ... # that represent integers + ... if not isinstance(value, (int, long, basestring)): + ... raise VdtTypeError(value) + ... elif isinstance(value, basestring): + ... # if we are given a string + ... # attempt to convert to an integer + ... try: + ... value = int(value) + ... except ValueError: + ... raise VdtValueError(value) + ... # check the value is between our constraints + ... if not min <= value: + ... raise VdtValueTooSmallError(value) + ... if not value <= max: + ... raise VdtValueTooBigError(value) + ... return value + + >>> fdict = {'int_range': int_range_check} + >>> vtr1 = Validator(fdict) + >>> vtr1.check('int_range(20, 40)', '30') + 30 + >>> vtr1.check('int_range(20, 40)', '60') + Traceback (most recent call last): + VdtValueTooBigError: the value "60" is too big. + + New functions can be added with : :: + + >>> vtr2 = Validator() + >>> vtr2.functions['int_range'] = int_range_check + + Or by passing in a dictionary of functions when Validator + is instantiated. + + Your functions *can* use keyword arguments, + but the first argument should always be 'value'. + + If the function doesn't take additional arguments, + the parentheses are optional in the check. + It can be written with either of : :: + + keyword = function_name + keyword = function_name() + + The first program to utilise Validator() was Michael Foord's + ConfigObj, an alternative to ConfigParser which supports lists and + can validate a config file using a config schema. + For more details on using Validator with ConfigObj see: + http://www.voidspace.org.uk/python/configobj.html + """ + + # this regex does the initial parsing of the checks + _func_re = re.compile(r'(.+?)\((.*)\)', re.DOTALL) + + # this regex takes apart keyword arguments + _key_arg = re.compile(r'^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.*)$', re.DOTALL) + + + # this regex finds keyword=list(....) type values + _list_arg = _list_arg + + # this regex takes individual values out of lists - in one pass + _list_members = _list_members + + # These regexes check a set of arguments for validity + # and then pull the members out + _paramfinder = re.compile(_paramstring, re.VERBOSE | re.DOTALL) + _matchfinder = re.compile(_matchstring, re.VERBOSE | re.DOTALL) + + + def __init__(self, functions=None): + """ + >>> vtri = Validator() + """ + self.functions = { + '': self._pass, + 'integer': is_integer, + 'float': is_float, + 'boolean': is_boolean, + 'ip_addr': is_ip_addr, + 'string': is_string, + 'list': is_list, + 'tuple': is_tuple, + 'int_list': is_int_list, + 'float_list': is_float_list, + 'bool_list': is_bool_list, + 'ip_addr_list': is_ip_addr_list, + 'string_list': is_string_list, + 'mixed_list': is_mixed_list, + 'pass': self._pass, + 'option': is_option, + 'force_list': force_list, + } + if functions is not None: + self.functions.update(functions) + # tekNico: for use by ConfigObj + self.baseErrorClass = ValidateError + self._cache = {} + + + def check(self, check, value, missing=False): + """ + Usage: check(check, value) + + Arguments: + check: string representing check to apply (including arguments) + value: object to be checked + Returns value, converted to correct type if necessary + + If the check fails, raises a ``ValidateError`` subclass. + + >>> vtor.check('yoda', '') + Traceback (most recent call last): + VdtUnknownCheckError: the check "yoda" is unknown. + >>> vtor.check('yoda()', '') + Traceback (most recent call last): + VdtUnknownCheckError: the check "yoda" is unknown. + + >>> vtor.check('string(default="")', '', missing=True) + '' + """ + fun_name, fun_args, fun_kwargs, default = self._parse_with_caching(check) + + if missing: + if default is None: + # no information needed here - to be handled by caller + raise VdtMissingValue() + value = self._handle_none(default) + + if value is None: + return None + + return self._check_value(value, fun_name, fun_args, fun_kwargs) + + + def _handle_none(self, value): + if value == 'None': + value = None + elif value in ("'None'", '"None"'): + # Special case a quoted None + value = self._unquote(value) + return value + + + def _parse_with_caching(self, check): + if check in self._cache: + fun_name, fun_args, fun_kwargs, default = self._cache[check] + # We call list and dict below to work with *copies* of the data + # rather than the original (which are mutable of course) + fun_args = list(fun_args) + fun_kwargs = dict(fun_kwargs) + else: + fun_name, fun_args, fun_kwargs, default = self._parse_check(check) + fun_kwargs = dict((str(key), value) for (key, value) in fun_kwargs.items()) + self._cache[check] = fun_name, list(fun_args), dict(fun_kwargs), default + return fun_name, fun_args, fun_kwargs, default + + + def _check_value(self, value, fun_name, fun_args, fun_kwargs): + try: + fun = self.functions[fun_name] + except KeyError: + raise VdtUnknownCheckError(fun_name) + else: + return fun(value, *fun_args, **fun_kwargs) + + + def _parse_check(self, check): + fun_match = self._func_re.match(check) + if fun_match: + fun_name = fun_match.group(1) + arg_string = fun_match.group(2) + arg_match = self._matchfinder.match(arg_string) + if arg_match is None: + # Bad syntax + raise VdtParamError('Bad syntax in check "%s".' % check) + fun_args = [] + fun_kwargs = {} + # pull out args of group 2 + for arg in self._paramfinder.findall(arg_string): + # args may need whitespace removing (before removing quotes) + arg = arg.strip() + listmatch = self._list_arg.match(arg) + if listmatch: + key, val = self._list_handle(listmatch) + fun_kwargs[key] = val + continue + keymatch = self._key_arg.match(arg) + if keymatch: + val = keymatch.group(2) + if not val in ("'None'", '"None"'): + # Special case a quoted None + val = self._unquote(val) + fun_kwargs[keymatch.group(1)] = val + continue + + fun_args.append(self._unquote(arg)) + else: + # allows for function names without (args) + return check, (), {}, None + + # Default must be deleted if the value is specified too, + # otherwise the check function will get a spurious "default" keyword arg + try: + default = fun_kwargs.pop('default', None) + except AttributeError: + # Python 2.2 compatibility + default = None + try: + default = fun_kwargs['default'] + del fun_kwargs['default'] + except KeyError: + pass + + return fun_name, fun_args, fun_kwargs, default + + + def _unquote(self, val): + """Unquote a value if necessary.""" + if (len(val) >= 2) and (val[0] in ("'", '"')) and (val[0] == val[-1]): + val = val[1:-1] + return val + + + def _list_handle(self, listmatch): + """Take apart a ``keyword=list('val, 'val')`` type string.""" + out = [] + name = listmatch.group(1) + args = listmatch.group(2) + for arg in self._list_members.findall(args): + out.append(self._unquote(arg)) + return name, out + + + def _pass(self, value): + """ + Dummy check that always passes + + >>> vtor.check('', 0) + 0 + >>> vtor.check('', '0') + '0' + """ + return value + + + def get_default_value(self, check): + """ + Given a check, return the default value for the check + (converted to the right type). + + If the check doesn't specify a default value then a + ``KeyError`` will be raised. + """ + fun_name, fun_args, fun_kwargs, default = self._parse_with_caching(check) + if default is None: + raise KeyError('Check "%s" has no default value.' % check) + value = self._handle_none(default) + if value is None: + return value + return self._check_value(value, fun_name, fun_args, fun_kwargs) + + +def _is_num_param(names, values, to_float=False): + """ + Return numbers from inputs or raise VdtParamError. + + Lets ``None`` pass through. + Pass in keyword argument ``to_float=True`` to + use float for the conversion rather than int. + + >>> _is_num_param(('', ''), (0, 1.0)) + [0, 1] + >>> _is_num_param(('', ''), (0, 1.0), to_float=True) + [0.0, 1.0] + >>> _is_num_param(('a'), ('a')) + Traceback (most recent call last): + VdtParamError: passed an incorrect value "a" for parameter "a". + """ + fun = to_float and float or int + out_params = [] + for (name, val) in zip(names, values): + if val is None: + out_params.append(val) + elif isinstance(val, (int, long, float, basestring)): + try: + out_params.append(fun(val)) + except ValueError, e: + raise VdtParamError(name, val) + else: + raise VdtParamError(name, val) + return out_params + + +# built in checks +# you can override these by setting the appropriate name +# in Validator.functions +# note: if the params are specified wrongly in your input string, +# you will also raise errors. + +def is_integer(value, min=None, max=None): + """ + A check that tests that a given value is an integer (int, or long) + and optionally, between bounds. A negative value is accepted, while + a float will fail. + + If the value is a string, then the conversion is done - if possible. + Otherwise a VdtError is raised. + + >>> vtor.check('integer', '-1') + -1 + >>> vtor.check('integer', '0') + 0 + >>> vtor.check('integer', 9) + 9 + >>> vtor.check('integer', 'a') + Traceback (most recent call last): + VdtTypeError: the value "a" is of the wrong type. + >>> vtor.check('integer', '2.2') + Traceback (most recent call last): + VdtTypeError: the value "2.2" is of the wrong type. + >>> vtor.check('integer(10)', '20') + 20 + >>> vtor.check('integer(max=20)', '15') + 15 + >>> vtor.check('integer(10)', '9') + Traceback (most recent call last): + VdtValueTooSmallError: the value "9" is too small. + >>> vtor.check('integer(10)', 9) + Traceback (most recent call last): + VdtValueTooSmallError: the value "9" is too small. + >>> vtor.check('integer(max=20)', '35') + Traceback (most recent call last): + VdtValueTooBigError: the value "35" is too big. + >>> vtor.check('integer(max=20)', 35) + Traceback (most recent call last): + VdtValueTooBigError: the value "35" is too big. + >>> vtor.check('integer(0, 9)', False) + 0 + """ + (min_val, max_val) = _is_num_param(('min', 'max'), (min, max)) + if not isinstance(value, (int, long, basestring)): + raise VdtTypeError(value) + if isinstance(value, basestring): + # if it's a string - does it represent an integer ? + try: + value = int(value) + except ValueError: + raise VdtTypeError(value) + if (min_val is not None) and (value < min_val): + raise VdtValueTooSmallError(value) + if (max_val is not None) and (value > max_val): + raise VdtValueTooBigError(value) + return value + + +def is_float(value, min=None, max=None): + """ + A check that tests that a given value is a float + (an integer will be accepted), and optionally - that it is between bounds. + + If the value is a string, then the conversion is done - if possible. + Otherwise a VdtError is raised. + + This can accept negative values. + + >>> vtor.check('float', '2') + 2.0 + + From now on we multiply the value to avoid comparing decimals + + >>> vtor.check('float', '-6.8') * 10 + -68.0 + >>> vtor.check('float', '12.2') * 10 + 122.0 + >>> vtor.check('float', 8.4) * 10 + 84.0 + >>> vtor.check('float', 'a') + Traceback (most recent call last): + VdtTypeError: the value "a" is of the wrong type. + >>> vtor.check('float(10.1)', '10.2') * 10 + 102.0 + >>> vtor.check('float(max=20.2)', '15.1') * 10 + 151.0 + >>> vtor.check('float(10.0)', '9.0') + Traceback (most recent call last): + VdtValueTooSmallError: the value "9.0" is too small. + >>> vtor.check('float(max=20.0)', '35.0') + Traceback (most recent call last): + VdtValueTooBigError: the value "35.0" is too big. + """ + (min_val, max_val) = _is_num_param( + ('min', 'max'), (min, max), to_float=True) + if not isinstance(value, (int, long, float, basestring)): + raise VdtTypeError(value) + if not isinstance(value, float): + # if it's a string - does it represent a float ? + try: + value = float(value) + except ValueError: + raise VdtTypeError(value) + if (min_val is not None) and (value < min_val): + raise VdtValueTooSmallError(value) + if (max_val is not None) and (value > max_val): + raise VdtValueTooBigError(value) + return value + + +bool_dict = { + True: True, 'on': True, '1': True, 'true': True, 'yes': True, + False: False, 'off': False, '0': False, 'false': False, 'no': False, +} + + +def is_boolean(value): + """ + Check if the value represents a boolean. + + >>> vtor.check('boolean', 0) + 0 + >>> vtor.check('boolean', False) + 0 + >>> vtor.check('boolean', '0') + 0 + >>> vtor.check('boolean', 'off') + 0 + >>> vtor.check('boolean', 'false') + 0 + >>> vtor.check('boolean', 'no') + 0 + >>> vtor.check('boolean', 'nO') + 0 + >>> vtor.check('boolean', 'NO') + 0 + >>> vtor.check('boolean', 1) + 1 + >>> vtor.check('boolean', True) + 1 + >>> vtor.check('boolean', '1') + 1 + >>> vtor.check('boolean', 'on') + 1 + >>> vtor.check('boolean', 'true') + 1 + >>> vtor.check('boolean', 'yes') + 1 + >>> vtor.check('boolean', 'Yes') + 1 + >>> vtor.check('boolean', 'YES') + 1 + >>> vtor.check('boolean', '') + Traceback (most recent call last): + VdtTypeError: the value "" is of the wrong type. + >>> vtor.check('boolean', 'up') + Traceback (most recent call last): + VdtTypeError: the value "up" is of the wrong type. + + """ + if isinstance(value, basestring): + try: + return bool_dict[value.lower()] + except KeyError: + raise VdtTypeError(value) + # we do an equality test rather than an identity test + # this ensures Python 2.2 compatibilty + # and allows 0 and 1 to represent True and False + if value == False: + return False + elif value == True: + return True + else: + raise VdtTypeError(value) + + +def is_ip_addr(value): + """ + Check that the supplied value is an Internet Protocol address, v.4, + represented by a dotted-quad string, i.e. '1.2.3.4'. + + >>> vtor.check('ip_addr', '1 ') + '1' + >>> vtor.check('ip_addr', ' 1.2') + '1.2' + >>> vtor.check('ip_addr', ' 1.2.3 ') + '1.2.3' + >>> vtor.check('ip_addr', '1.2.3.4') + '1.2.3.4' + >>> vtor.check('ip_addr', '0.0.0.0') + '0.0.0.0' + >>> vtor.check('ip_addr', '255.255.255.255') + '255.255.255.255' + >>> vtor.check('ip_addr', '255.255.255.256') + Traceback (most recent call last): + VdtValueError: the value "255.255.255.256" is unacceptable. + >>> vtor.check('ip_addr', '1.2.3.4.5') + Traceback (most recent call last): + VdtValueError: the value "1.2.3.4.5" is unacceptable. + >>> vtor.check('ip_addr', 0) + Traceback (most recent call last): + VdtTypeError: the value "0" is of the wrong type. + """ + if not isinstance(value, basestring): + raise VdtTypeError(value) + value = value.strip() + try: + dottedQuadToNum(value) + except ValueError: + raise VdtValueError(value) + return value + + +def is_list(value, min=None, max=None): + """ + Check that the value is a list of values. + + You can optionally specify the minimum and maximum number of members. + + It does no check on list members. + + >>> vtor.check('list', ()) + [] + >>> vtor.check('list', []) + [] + >>> vtor.check('list', (1, 2)) + [1, 2] + >>> vtor.check('list', [1, 2]) + [1, 2] + >>> vtor.check('list(3)', (1, 2)) + Traceback (most recent call last): + VdtValueTooShortError: the value "(1, 2)" is too short. + >>> vtor.check('list(max=5)', (1, 2, 3, 4, 5, 6)) + Traceback (most recent call last): + VdtValueTooLongError: the value "(1, 2, 3, 4, 5, 6)" is too long. + >>> vtor.check('list(min=3, max=5)', (1, 2, 3, 4)) + [1, 2, 3, 4] + >>> vtor.check('list', 0) + Traceback (most recent call last): + VdtTypeError: the value "0" is of the wrong type. + >>> vtor.check('list', '12') + Traceback (most recent call last): + VdtTypeError: the value "12" is of the wrong type. + """ + (min_len, max_len) = _is_num_param(('min', 'max'), (min, max)) + if isinstance(value, basestring): + raise VdtTypeError(value) + try: + num_members = len(value) + except TypeError: + raise VdtTypeError(value) + if min_len is not None and num_members < min_len: + raise VdtValueTooShortError(value) + if max_len is not None and num_members > max_len: + raise VdtValueTooLongError(value) + return list(value) + + +def is_tuple(value, min=None, max=None): + """ + Check that the value is a tuple of values. + + You can optionally specify the minimum and maximum number of members. + + It does no check on members. + + >>> vtor.check('tuple', ()) + () + >>> vtor.check('tuple', []) + () + >>> vtor.check('tuple', (1, 2)) + (1, 2) + >>> vtor.check('tuple', [1, 2]) + (1, 2) + >>> vtor.check('tuple(3)', (1, 2)) + Traceback (most recent call last): + VdtValueTooShortError: the value "(1, 2)" is too short. + >>> vtor.check('tuple(max=5)', (1, 2, 3, 4, 5, 6)) + Traceback (most recent call last): + VdtValueTooLongError: the value "(1, 2, 3, 4, 5, 6)" is too long. + >>> vtor.check('tuple(min=3, max=5)', (1, 2, 3, 4)) + (1, 2, 3, 4) + >>> vtor.check('tuple', 0) + Traceback (most recent call last): + VdtTypeError: the value "0" is of the wrong type. + >>> vtor.check('tuple', '12') + Traceback (most recent call last): + VdtTypeError: the value "12" is of the wrong type. + """ + return tuple(is_list(value, min, max)) + + +def is_string(value, min=None, max=None): + """ + Check that the supplied value is a string. + + You can optionally specify the minimum and maximum number of members. + + >>> vtor.check('string', '0') + '0' + >>> vtor.check('string', 0) + Traceback (most recent call last): + VdtTypeError: the value "0" is of the wrong type. + >>> vtor.check('string(2)', '12') + '12' + >>> vtor.check('string(2)', '1') + Traceback (most recent call last): + VdtValueTooShortError: the value "1" is too short. + >>> vtor.check('string(min=2, max=3)', '123') + '123' + >>> vtor.check('string(min=2, max=3)', '1234') + Traceback (most recent call last): + VdtValueTooLongError: the value "1234" is too long. + """ + if not isinstance(value, basestring): + raise VdtTypeError(value) + (min_len, max_len) = _is_num_param(('min', 'max'), (min, max)) + try: + num_members = len(value) + except TypeError: + raise VdtTypeError(value) + if min_len is not None and num_members < min_len: + raise VdtValueTooShortError(value) + if max_len is not None and num_members > max_len: + raise VdtValueTooLongError(value) + return value + + +def is_int_list(value, min=None, max=None): + """ + Check that the value is a list of integers. + + You can optionally specify the minimum and maximum number of members. + + Each list member is checked that it is an integer. + + >>> vtor.check('int_list', ()) + [] + >>> vtor.check('int_list', []) + [] + >>> vtor.check('int_list', (1, 2)) + [1, 2] + >>> vtor.check('int_list', [1, 2]) + [1, 2] + >>> vtor.check('int_list', [1, 'a']) + Traceback (most recent call last): + VdtTypeError: the value "a" is of the wrong type. + """ + return [is_integer(mem) for mem in is_list(value, min, max)] + + +def is_bool_list(value, min=None, max=None): + """ + Check that the value is a list of booleans. + + You can optionally specify the minimum and maximum number of members. + + Each list member is checked that it is a boolean. + + >>> vtor.check('bool_list', ()) + [] + >>> vtor.check('bool_list', []) + [] + >>> check_res = vtor.check('bool_list', (True, False)) + >>> check_res == [True, False] + 1 + >>> check_res = vtor.check('bool_list', [True, False]) + >>> check_res == [True, False] + 1 + >>> vtor.check('bool_list', [True, 'a']) + Traceback (most recent call last): + VdtTypeError: the value "a" is of the wrong type. + """ + return [is_boolean(mem) for mem in is_list(value, min, max)] + + +def is_float_list(value, min=None, max=None): + """ + Check that the value is a list of floats. + + You can optionally specify the minimum and maximum number of members. + + Each list member is checked that it is a float. + + >>> vtor.check('float_list', ()) + [] + >>> vtor.check('float_list', []) + [] + >>> vtor.check('float_list', (1, 2.0)) + [1.0, 2.0] + >>> vtor.check('float_list', [1, 2.0]) + [1.0, 2.0] + >>> vtor.check('float_list', [1, 'a']) + Traceback (most recent call last): + VdtTypeError: the value "a" is of the wrong type. + """ + return [is_float(mem) for mem in is_list(value, min, max)] + + +def is_string_list(value, min=None, max=None): + """ + Check that the value is a list of strings. + + You can optionally specify the minimum and maximum number of members. + + Each list member is checked that it is a string. + + >>> vtor.check('string_list', ()) + [] + >>> vtor.check('string_list', []) + [] + >>> vtor.check('string_list', ('a', 'b')) + ['a', 'b'] + >>> vtor.check('string_list', ['a', 1]) + Traceback (most recent call last): + VdtTypeError: the value "1" is of the wrong type. + >>> vtor.check('string_list', 'hello') + Traceback (most recent call last): + VdtTypeError: the value "hello" is of the wrong type. + """ + if isinstance(value, basestring): + raise VdtTypeError(value) + return [is_string(mem) for mem in is_list(value, min, max)] + + +def is_ip_addr_list(value, min=None, max=None): + """ + Check that the value is a list of IP addresses. + + You can optionally specify the minimum and maximum number of members. + + Each list member is checked that it is an IP address. + + >>> vtor.check('ip_addr_list', ()) + [] + >>> vtor.check('ip_addr_list', []) + [] + >>> vtor.check('ip_addr_list', ('1.2.3.4', '5.6.7.8')) + ['1.2.3.4', '5.6.7.8'] + >>> vtor.check('ip_addr_list', ['a']) + Traceback (most recent call last): + VdtValueError: the value "a" is unacceptable. + """ + return [is_ip_addr(mem) for mem in is_list(value, min, max)] + + +def force_list(value, min=None, max=None): + """ + Check that a value is a list, coercing strings into + a list with one member. Useful where users forget the + trailing comma that turns a single value into a list. + + You can optionally specify the minimum and maximum number of members. + A minumum of greater than one will fail if the user only supplies a + string. + + >>> vtor.check('force_list', ()) + [] + >>> vtor.check('force_list', []) + [] + >>> vtor.check('force_list', 'hello') + ['hello'] + """ + if not isinstance(value, (list, tuple)): + value = [value] + return is_list(value, min, max) + + + +fun_dict = { + 'integer': is_integer, + 'float': is_float, + 'ip_addr': is_ip_addr, + 'string': is_string, + 'boolean': is_boolean, +} + + +def is_mixed_list(value, *args): + """ + Check that the value is a list. + Allow specifying the type of each member. + Work on lists of specific lengths. + + You specify each member as a positional argument specifying type + + Each type should be one of the following strings : + 'integer', 'float', 'ip_addr', 'string', 'boolean' + + So you can specify a list of two strings, followed by + two integers as : + + mixed_list('string', 'string', 'integer', 'integer') + + The length of the list must match the number of positional + arguments you supply. + + >>> mix_str = "mixed_list('integer', 'float', 'ip_addr', 'string', 'boolean')" + >>> check_res = vtor.check(mix_str, (1, 2.0, '1.2.3.4', 'a', True)) + >>> check_res == [1, 2.0, '1.2.3.4', 'a', True] + 1 + >>> check_res = vtor.check(mix_str, ('1', '2.0', '1.2.3.4', 'a', 'True')) + >>> check_res == [1, 2.0, '1.2.3.4', 'a', True] + 1 + >>> vtor.check(mix_str, ('b', 2.0, '1.2.3.4', 'a', True)) + Traceback (most recent call last): + VdtTypeError: the value "b" is of the wrong type. + >>> vtor.check(mix_str, (1, 2.0, '1.2.3.4', 'a')) + Traceback (most recent call last): + VdtValueTooShortError: the value "(1, 2.0, '1.2.3.4', 'a')" is too short. + >>> vtor.check(mix_str, (1, 2.0, '1.2.3.4', 'a', 1, 'b')) + Traceback (most recent call last): + VdtValueTooLongError: the value "(1, 2.0, '1.2.3.4', 'a', 1, 'b')" is too long. + >>> vtor.check(mix_str, 0) + Traceback (most recent call last): + VdtTypeError: the value "0" is of the wrong type. + + This test requires an elaborate setup, because of a change in error string + output from the interpreter between Python 2.2 and 2.3 . + + >>> res_seq = ( + ... 'passed an incorrect value "', + ... 'yoda', + ... '" for parameter "mixed_list".', + ... ) + >>> res_str = "'".join(res_seq) + >>> try: + ... vtor.check('mixed_list("yoda")', ('a')) + ... except VdtParamError, err: + ... str(err) == res_str + 1 + """ + try: + length = len(value) + except TypeError: + raise VdtTypeError(value) + if length < len(args): + raise VdtValueTooShortError(value) + elif length > len(args): + raise VdtValueTooLongError(value) + try: + return [fun_dict[arg](val) for arg, val in zip(args, value)] + except KeyError, e: + raise VdtParamError('mixed_list', e) + + +def is_option(value, *options): + """ + This check matches the value to any of a set of options. + + >>> vtor.check('option("yoda", "jedi")', 'yoda') + 'yoda' + >>> vtor.check('option("yoda", "jedi")', 'jed') + Traceback (most recent call last): + VdtValueError: the value "jed" is unacceptable. + >>> vtor.check('option("yoda", "jedi")', 0) + Traceback (most recent call last): + VdtTypeError: the value "0" is of the wrong type. + """ + if not isinstance(value, basestring): + raise VdtTypeError(value) + if not value in options: + raise VdtValueError(value) + return value + + +def _test(value, *args, **keywargs): + """ + A function that exists for test purposes. + + >>> checks = [ + ... '3, 6, min=1, max=3, test=list(a, b, c)', + ... '3', + ... '3, 6', + ... '3,', + ... 'min=1, test="a b c"', + ... 'min=5, test="a, b, c"', + ... 'min=1, max=3, test="a, b, c"', + ... 'min=-100, test=-99', + ... 'min=1, max=3', + ... '3, 6, test="36"', + ... '3, 6, test="a, b, c"', + ... '3, max=3, test=list("a", "b", "c")', + ... '''3, max=3, test=list("'a'", 'b', "x=(c)")''', + ... "test='x=fish(3)'", + ... ] + >>> v = Validator({'test': _test}) + >>> for entry in checks: + ... print v.check(('test(%s)' % entry), 3) + (3, ('3', '6'), {'test': ['a', 'b', 'c'], 'max': '3', 'min': '1'}) + (3, ('3',), {}) + (3, ('3', '6'), {}) + (3, ('3',), {}) + (3, (), {'test': 'a b c', 'min': '1'}) + (3, (), {'test': 'a, b, c', 'min': '5'}) + (3, (), {'test': 'a, b, c', 'max': '3', 'min': '1'}) + (3, (), {'test': '-99', 'min': '-100'}) + (3, (), {'max': '3', 'min': '1'}) + (3, ('3', '6'), {'test': '36'}) + (3, ('3', '6'), {'test': 'a, b, c'}) + (3, ('3',), {'test': ['a', 'b', 'c'], 'max': '3'}) + (3, ('3',), {'test': ["'a'", 'b', 'x=(c)'], 'max': '3'}) + (3, (), {'test': 'x=fish(3)'}) + + >>> v = Validator() + >>> v.check('integer(default=6)', '3') + 3 + >>> v.check('integer(default=6)', None, True) + 6 + >>> v.get_default_value('integer(default=6)') + 6 + >>> v.get_default_value('float(default=6)') + 6.0 + >>> v.get_default_value('pass(default=None)') + >>> v.get_default_value("string(default='None')") + 'None' + >>> v.get_default_value('pass') + Traceback (most recent call last): + KeyError: 'Check "pass" has no default value.' + >>> v.get_default_value('pass(default=list(1, 2, 3, 4))') + ['1', '2', '3', '4'] + + >>> v = Validator() + >>> v.check("pass(default=None)", None, True) + >>> v.check("pass(default='None')", None, True) + 'None' + >>> v.check('pass(default="None")', None, True) + 'None' + >>> v.check('pass(default=list(1, 2, 3, 4))', None, True) + ['1', '2', '3', '4'] + + Bug test for unicode arguments + >>> v = Validator() + >>> v.check(u'string(min=4)', u'test') + u'test' + + >>> v = Validator() + >>> v.get_default_value(u'string(min=4, default="1234")') + u'1234' + >>> v.check(u'string(min=4, default="1234")', u'test') + u'test' + + >>> v = Validator() + >>> default = v.get_default_value('string(default=None)') + >>> default == None + 1 + """ + return (value, args, keywargs) + + +def _test2(): + """ + >>> + >>> v = Validator() + >>> v.get_default_value('string(default="#ff00dd")') + '#ff00dd' + >>> v.get_default_value('integer(default=3) # comment') + 3 + """ + +def _test3(): + r""" + >>> vtor.check('string(default="")', '', missing=True) + '' + >>> vtor.check('string(default="\n")', '', missing=True) + '\n' + >>> print vtor.check('string(default="\n")', '', missing=True), + + >>> vtor.check('string()', '\n') + '\n' + >>> vtor.check('string(default="\n\n\n")', '', missing=True) + '\n\n\n' + >>> vtor.check('string()', 'random \n text goes here\n\n') + 'random \n text goes here\n\n' + >>> vtor.check('string(default=" \nrandom text\ngoes \n here\n\n ")', + ... '', missing=True) + ' \nrandom text\ngoes \n here\n\n ' + >>> vtor.check("string(default='\n\n\n')", '', missing=True) + '\n\n\n' + >>> vtor.check("option('\n','a','b',default='\n')", '', missing=True) + '\n' + >>> vtor.check("string_list()", ['foo', '\n', 'bar']) + ['foo', '\n', 'bar'] + >>> vtor.check("string_list(default=list('\n'))", '', missing=True) + ['\n'] + """ + + +if __name__ == '__main__': + # run the code tests in doctest format + import sys + import doctest + m = sys.modules.get('__main__') + globs = m.__dict__.copy() + globs.update({ + 'vtor': Validator(), + }) + doctest.testmod(m, globs=globs) diff --git a/terminatorlib/container.py b/terminatorlib/container.py new file mode 100755 index 00000000..175da7c3 --- /dev/null +++ b/terminatorlib/container.py @@ -0,0 +1,192 @@ +#!/usr/bin/python +# Terminator by Chris Jones +# GPL v2 only +"""container.py - classes necessary to contain Terminal widgets""" + +import gobject +import gtk + +from factory import Factory +from config import Config +from util import dbg, err +from translation import _ +from signalman import Signalman + +# pylint: disable-msg=R0921 +class Container(object): + """Base class for Terminator Containers""" + + terminator = None + immutable = None + children = None + config = None + signals = None + signalman = None + + def __init__(self): + """Class initialiser""" + self.children = [] + self.signals = [] + self.cnxids = Signalman() + self.config = Config() + + def register_signals(self, widget): + """Register gobject signals in a way that avoids multiple inheritance""" + existing = gobject.signal_list_names(widget) + for signal in self.signals: + if signal['name'] in existing: + dbg('Container:: skipping signal %s for %s, already exists' % ( + signal['name'], widget)) + else: + dbg('Container:: registering signal for %s on %s' % + (signal['name'], widget)) + try: + gobject.signal_new(signal['name'], + widget, + signal['flags'], + signal['return_type'], + signal['param_types']) + except RuntimeError: + err('Container:: registering signal for %s on %s failed' % + (signal['name'], widget)) + + def connect_child(self, widget, signal, handler, *args): + """Register the requested signal and record its connection ID""" + self.cnxids.new(widget, signal, handler, *args) + return + + def disconnect_child(self, widget): + """De-register the signals for a child""" + self.cnxids.remove_widget(widget) + + def get_offspring(self): + """Return a list of child widgets, if any""" + return(self.children) + + def split_horiz(self, widget): + """Split this container horizontally""" + return(self.split_axis(widget, True)) + + def split_vert(self, widget): + """Split this container vertically""" + return(self.split_axis(widget, False)) + + def split_axis(self, widget, vertical=True, sibling=None): + """Default axis splitter. This should be implemented by subclasses""" + raise NotImplementedError('split_axis') + + def add(self, widget): + """Add a widget to the container""" + raise NotImplementedError('add') + + def remove(self, widget): + """Remove a widget from the container""" + raise NotImplementedError('remove') + + def closeterm(self, widget): + """Handle the closure of a terminal""" + try: + if self.get_property('term_zoomed'): + # We're zoomed, so unzoom and then start closing again + dbg('Container::closeterm: terminal zoomed, unzooming') + self.unzoom(widget) + widget.close() + return(True) + except TypeError: + pass + + if not self.remove(widget): + dbg('Container::closeterm: self.remove() failed for %s' % widget) + return(False) + + self.terminator.deregister_terminal(widget) + self.terminator.group_hoover() + return(True) + + def resizeterm(self, widget, keyname): + """Handle a keyboard event requesting a terminal resize""" + raise NotImplementedError('resizeterm') + + def toggle_zoom(self, widget, fontscale = False): + """Toggle the existing zoom state""" + try: + if self.get_property('term_zoomed'): + self.unzoom(widget) + else: + self.zoom(widget, fontscale) + except TypeError: + err('Container::toggle_zoom: %s is unable to handle zooming, for \ + %s' % (self, widget)) + + def zoom(self, widget, fontscale = False): + """Zoom a terminal""" + raise NotImplementedError('zoom') + + def unzoom(self, widget): + """Unzoom a terminal""" + raise NotImplementedError('unzoom') + + def construct_confirm_close(self, window, reqtype): + """Create a confirmation dialog for closing things""" + dialog = gtk.Dialog(_('Close?'), window, gtk.DIALOG_MODAL) + dialog.set_has_separator(False) + dialog.set_resizable(False) + + dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT) + c_all = dialog.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_ACCEPT) + c_all.get_children()[0].get_children()[0].get_children()[1].set_label(_('Close _Terminals')) + + primary = gtk.Label(_('Close multiple terminals?')) + primary.set_use_markup(True) + primary.set_alignment(0, 0.5) + secondary = gtk.Label(_('This %s has several terminals open. Closing \ +the %s will also close all terminals within it.') % (reqtype, reqtype)) + secondary.set_line_wrap(True) + + labels = gtk.VBox() + labels.pack_start(primary, False, False, 6) + labels.pack_start(secondary, False, False, 6) + + image = gtk.image_new_from_stock(gtk.STOCK_DIALOG_WARNING, + gtk.ICON_SIZE_DIALOG) + image.set_alignment(0.5, 0) + + box = gtk.HBox() + box.pack_start(image, False, False, 6) + box.pack_start(labels, False, False, 6) + dialog.vbox.pack_start(box, False, False, 12) + + dialog.show_all() + return(dialog) + + def propagate_title_change(self, widget, title): + """Pass a title change up the widget stack""" + maker = Factory() + parent = self.get_parent() + title = widget.get_window_title() + + if maker.isinstance(self, 'Notebook'): + self.update_tab_label_text(widget, title) + elif maker.isinstance(self, 'Window'): + self.title.set_title(widget, title) + + if maker.isinstance(parent, 'Container'): + parent.propagate_title_change(widget, title) + + def get_visible_terminals(self): + """Walk the widget tree to find all of the visible terminals. That is, + any terminals which are not hidden in another Notebook pane""" + maker = Factory() + terminals = {} + + for child in self.get_offspring(): + if maker.isinstance(child, 'Terminal'): + terminals[child] = child.get_allocation() + elif maker.isinstance(child, 'Container'): + terminals.update(child.get_visible_terminals()) + else: + err('Unknown child type %s' % type(child)) + + return(terminals) + +# vim: set expandtab ts=4 sw=4: diff --git a/terminatorlib/cwd.py b/terminatorlib/cwd.py new file mode 100755 index 00000000..1edb6fb9 --- /dev/null +++ b/terminatorlib/cwd.py @@ -0,0 +1,52 @@ +#!/usr/bin/python +# Terminator by Chris Jones +# GPL v2 only +"""cwd.py - function necessary to get the cwd for a given pid on various OSes + +>>> cwd = get_default_cwd() +>>> cwd.__class__.__name__ +'str' +>>> func = get_pid_cwd() +>>> func.__class__.__name__ +'function' + +""" + +import platform +import os +import pwd +from util import dbg + +def get_default_cwd(): + """Determine a reasonable default cwd""" + cwd = os.getcwd() + if not os.path.exists(cwd) or not os.path.isdir(cwd): + cwd = pwd.getpwuid(os.getuid())[5] + + return(cwd) + +def get_pid_cwd(): + """Determine an appropriate cwd function for the OS we are running on""" + + func = lambda pid: None + system = platform.system() + + if system == 'Linux': + dbg('Using Linux get_pid_cwd') + func = lambda pid: os.path.realpath('/proc/%s/cwd' % pid) + elif system == 'FreeBSD': + try: + import freebsd + func = freebsd.get_process_cwd + dbg('Using FreeBSD get_pid_cwd') + except (OSError, NotImplementedError, ImportError): + dbg('FreeBSD version too old for get_pid_cwd') + elif system == 'SunOS': + dbg('Using SunOS get_pid_cwd') + func = lambda pid: os.path.realpath('/proc/%s/path/cwd' % pid) + else: + dbg('Unable to determine a get_pid_cwd for OS: %s' % system) + + return(func) + +# vim: set expandtab ts=4 sw=4: diff --git a/terminatorlib/debugserver.py b/terminatorlib/debugserver.py index bcb13d85..2621331a 100644 --- a/terminatorlib/debugserver.py +++ b/terminatorlib/debugserver.py @@ -5,7 +5,7 @@ # Use of this file is unrestricted provided this notice is retained. # If you use it, it'd be nice if you dropped me a note. Also beer. -from terminatorlib.config import dbg, err +from terminatorlib.util import dbg, err from terminatorlib.version import APP_NAME, APP_VERSION import socket diff --git a/terminatorlib/terminatoreditablelabel.py b/terminatorlib/editablelabel.py similarity index 73% rename from terminatorlib/terminatoreditablelabel.py rename to terminatorlib/editablelabel.py index d775636f..168f3157 100644 --- a/terminatorlib/terminatoreditablelabel.py +++ b/terminatorlib/editablelabel.py @@ -17,24 +17,33 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor # , Boston, MA 02110-1301 USA -# pylint: disable-msg=W0212 -''' Editable Label class''' +""" Editable Label class""" import gtk +import gobject -class TerminatorEditableLabel( gtk.EventBox ): - ''' +class EditableLabel(gtk.EventBox): + # pylint: disable-msg=W0212 + # pylint: disable-msg=R0904 + """ An eventbox that partialy emulate a gtk.Label On double-click, the label is editable, entering an empty will revert back to automatic text - ''' + """ _label = None _ebox = None _autotext = None _custom = None _entry = None _entry_handler_id = None + + __gsignals__ = { + 'edit-done': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + } + def __init__(self, text = ""): - ''' Class initialiser''' + """ Class initialiser""" gtk.EventBox.__init__(self) + self.__gobject_init__() + self._entry_handler_id = [] self._label = gtk.Label(text) self._custom = False @@ -43,22 +52,27 @@ class TerminatorEditableLabel( gtk.EventBox ): self.connect ("button-press-event", self._on_click_text) def set_angle(self, angle ): - '''set angle of the label''' + """set angle of the label""" self._label.set_angle( angle ) - - def set_text( self, text, force=False): - '''set the text of the label''' + + def editing(self): + """Return if we are currently editing""" + return(self._entry != None) + + def set_text(self, text, force=False): + """set the text of the label""" self._autotext = text if not self._custom or force: self._label.set_text(text) - def get_text( self ): - '''get the text from the label''' - return self._label.get_text() + def get_text(self): + """get the text from the label""" + return(self._label.get_text()) def _on_click_text(self, widget, event): - '''event handling text edition''' - if event.type == gtk.gdk._2BUTTON_PRESS : + # pylint: disable-msg=W0613 + """event handling text edition""" + if event.type == gtk.gdk._2BUTTON_PRESS: self.remove (self._label) self._entry = gtk.Entry () self._entry.set_text (self._label.get_text ()) @@ -72,13 +86,12 @@ class TerminatorEditableLabel( gtk.EventBox ): self._on_entry_keypress) self._entry_handler_id.append(sig) self._entry.grab_focus () - return True - # make pylint happy - if 1 or widget or event: - return False + return(True) + return(False) def _entry_to_label (self, widget, event): - '''replace gtk.Entry by the gtk.Label''' + # pylint: disable-msg=W0613 + """replace gtk.Entry by the gtk.Label""" if self._entry and self._entry in self.get_children(): #disconnect signals to avoid segfault :s for sig in self._entry_handler_id: @@ -89,13 +102,13 @@ class TerminatorEditableLabel( gtk.EventBox ): self.add (self._label) self._entry = None self.show_all () - return True - #make pylint happy - if 1 or widget or event: - return False + self.emit('edit-done') + return(True) + return(False) def _on_entry_activated (self, widget): - '''get the text entered in gtk.Entry''' + # pylint: disable-msg=W0613 + """get the text entered in gtk.Entry""" entry = self._entry.get_text () label = self._label.get_text () if entry == '': @@ -106,20 +119,11 @@ class TerminatorEditableLabel( gtk.EventBox ): self._label.set_text (entry) self._entry_to_label (None, None) - # make pylint happy - if 1 or widget: - return - def _on_entry_keypress (self, widget, event): - '''handle keypressed in gtk.Entry''' + # pylint: disable-msg=W0613 + """handle keypressed in gtk.Entry""" key = gtk.gdk.keyval_name (event.keyval) if key == 'Escape': self._entry_to_label (None, None) - # make pylint happy - if 1 or widget or event: - return - - def modify_fg (self, state, color): - '''modify the foreground of our label''' - self._label.modify_fg(state, color) +gobject.type_register(EditableLabel) diff --git a/terminatorlib/encoding.py b/terminatorlib/encoding.py index e205497b..b0a5ec20 100644 --- a/terminatorlib/encoding.py +++ b/terminatorlib/encoding.py @@ -1,6 +1,6 @@ #!/usr/bin/python # TerminatorEncoding - charset encoding classes -# Copyright (C) 2006-2008 chantra@debuntu.org +# Copyright (C) 2006-2010 chantra@debuntu.org # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -23,95 +23,97 @@ This list is taken from gnome-terminal's src/terminal-encoding.c and src/encoding.c """ -from terminatorlib import translation +from translation import _ +#pylint: disable-msg=R0903 class TerminatorEncoding: - """Class to store encoding details""" + """Class to store encoding details""" # The commented out entries below are so marked because gnome-terminal has done # the same. - encodings = [ - [True, None, _("Current Locale")], - [False, "ISO-8859-1", _("Western")], - [False, "ISO-8859-2", _("Central European")], - [False, "ISO-8859-3", _("South European") ], - [False, "ISO-8859-4", _("Baltic") ], - [False, "ISO-8859-5", _("Cyrillic") ], - [False, "ISO-8859-6", _("Arabic") ], - [False, "ISO-8859-7", _("Greek") ], - [False, "ISO-8859-8", _("Hebrew Visual") ], - [False, "ISO-8859-8-I", _("Hebrew") ], - [False, "ISO-8859-9", _("Turkish") ], - [False, "ISO-8859-10", _("Nordic") ], - [False, "ISO-8859-13", _("Baltic") ], - [False, "ISO-8859-14", _("Celtic") ], - [False, "ISO-8859-15", _("Western") ], - [False, "ISO-8859-16", _("Romanian") ], -# [False, "UTF-7", _("Unicode") ], - [False, "UTF-8", _("Unicode") ], -# [False, "UTF-16", _("Unicode") ], -# [False, "UCS-2", _("Unicode") ], -# [False, "UCS-4", _("Unicode") ], - [False, "ARMSCII-8", _("Armenian") ], - [False, "BIG5", _("Chinese Traditional") ], - [False, "BIG5-HKSCS", _("Chinese Traditional") ], - [False, "CP866", _("Cyrillic/Russian") ], - [False, "EUC-JP", _("Japanese") ], - [False, "EUC-KR", _("Korean") ], - [False, "EUC-TW", _("Chinese Traditional") ], - [False, "GB18030", _("Chinese Simplified") ], - [False, "GB2312", _("Chinese Simplified") ], - [False, "GBK", _("Chinese Simplified") ], - [False, "GEORGIAN-PS", _("Georgian") ], - [False, "HZ", _("Chinese Simplified") ], - [False, "IBM850", _("Western") ], - [False, "IBM852", _("Central European") ], - [False, "IBM855", _("Cyrillic") ], - [False, "IBM857", _("Turkish") ], - [False, "IBM862", _("Hebrew") ], - [False, "IBM864", _("Arabic") ], - [False, "ISO-2022-JP", _("Japanese") ], - [False, "ISO-2022-KR", _("Korean") ], - [False, "ISO-IR-111", _("Cyrillic") ], -# [False, "JOHAB", _("Korean") ], - [False, "KOI8-R", _("Cyrillic") ], - [False, "KOI8-U", _("Cyrillic/Ukrainian") ], - [False, "MAC_ARABIC", _("Arabic") ], - [False, "MAC_CE", _("Central European") ], - [False, "MAC_CROATIAN", _("Croatian") ], - [False, "MAC-CYRILLIC", _("Cyrillic") ], - [False, "MAC_DEVANAGARI", _("Hindi") ], - [False, "MAC_FARSI", _("Persian") ], - [False, "MAC_GREEK", _("Greek") ], - [False, "MAC_GUJARATI", _("Gujarati") ], - [False, "MAC_GURMUKHI", _("Gurmukhi") ], - [False, "MAC_HEBREW", _("Hebrew") ], - [False, "MAC_ICELANDIC", _("Icelandic") ], - [False, "MAC_ROMAN", _("Western") ], - [False, "MAC_ROMANIAN", _("Romanian") ], - [False, "MAC_TURKISH", _("Turkish") ], - [False, "MAC_UKRAINIAN", _("Cyrillic/Ukrainian") ], - [False, "SHIFT-JIS", _("Japanese") ], - [False, "TCVN", _("Vietnamese") ], - [False, "TIS-620", _("Thai") ], - [False, "UHC", _("Korean") ], - [False, "VISCII", _("Vietnamese") ], - [False, "WINDOWS-1250", _("Central European") ], - [False, "WINDOWS-1251", _("Cyrillic") ], - [False, "WINDOWS-1252", _("Western") ], - [False, "WINDOWS-1253", _("Greek") ], - [False, "WINDOWS-1254", _("Turkish") ], - [False, "WINDOWS-1255", _("Hebrew") ], - [False, "WINDOWS-1256", _("Arabic") ], - [False, "WINDOWS-1257", _("Baltic") ], - [False, "WINDOWS-1258", _("Vietnamese") ] - ] + encodings = [ + [True, None, _("Current Locale")], + [False, "ISO-8859-1", _("Western")], + [False, "ISO-8859-2", _("Central European")], + [False, "ISO-8859-3", _("South European") ], + [False, "ISO-8859-4", _("Baltic") ], + [False, "ISO-8859-5", _("Cyrillic") ], + [False, "ISO-8859-6", _("Arabic") ], + [False, "ISO-8859-7", _("Greek") ], + [False, "ISO-8859-8", _("Hebrew Visual") ], + [False, "ISO-8859-8-I", _("Hebrew") ], + [False, "ISO-8859-9", _("Turkish") ], + [False, "ISO-8859-10", _("Nordic") ], + [False, "ISO-8859-13", _("Baltic") ], + [False, "ISO-8859-14", _("Celtic") ], + [False, "ISO-8859-15", _("Western") ], + [False, "ISO-8859-16", _("Romanian") ], + # [False, "UTF-7", _("Unicode") ], + [False, "UTF-8", _("Unicode") ], + # [False, "UTF-16", _("Unicode") ], + # [False, "UCS-2", _("Unicode") ], + # [False, "UCS-4", _("Unicode") ], + [False, "ARMSCII-8", _("Armenian") ], + [False, "BIG5", _("Chinese Traditional") ], + [False, "BIG5-HKSCS", _("Chinese Traditional") ], + [False, "CP866", _("Cyrillic/Russian") ], + [False, "EUC-JP", _("Japanese") ], + [False, "EUC-KR", _("Korean") ], + [False, "EUC-TW", _("Chinese Traditional") ], + [False, "GB18030", _("Chinese Simplified") ], + [False, "GB2312", _("Chinese Simplified") ], + [False, "GBK", _("Chinese Simplified") ], + [False, "GEORGIAN-PS", _("Georgian") ], + [False, "HZ", _("Chinese Simplified") ], + [False, "IBM850", _("Western") ], + [False, "IBM852", _("Central European") ], + [False, "IBM855", _("Cyrillic") ], + [False, "IBM857", _("Turkish") ], + [False, "IBM862", _("Hebrew") ], + [False, "IBM864", _("Arabic") ], + [False, "ISO-2022-JP", _("Japanese") ], + [False, "ISO-2022-KR", _("Korean") ], + [False, "ISO-IR-111", _("Cyrillic") ], + # [False, "JOHAB", _("Korean") ], + [False, "KOI8-R", _("Cyrillic") ], + [False, "KOI8-U", _("Cyrillic/Ukrainian") ], + [False, "MAC_ARABIC", _("Arabic") ], + [False, "MAC_CE", _("Central European") ], + [False, "MAC_CROATIAN", _("Croatian") ], + [False, "MAC-CYRILLIC", _("Cyrillic") ], + [False, "MAC_DEVANAGARI", _("Hindi") ], + [False, "MAC_FARSI", _("Persian") ], + [False, "MAC_GREEK", _("Greek") ], + [False, "MAC_GUJARATI", _("Gujarati") ], + [False, "MAC_GURMUKHI", _("Gurmukhi") ], + [False, "MAC_HEBREW", _("Hebrew") ], + [False, "MAC_ICELANDIC", _("Icelandic") ], + [False, "MAC_ROMAN", _("Western") ], + [False, "MAC_ROMANIAN", _("Romanian") ], + [False, "MAC_TURKISH", _("Turkish") ], + [False, "MAC_UKRAINIAN", _("Cyrillic/Ukrainian") ], + [False, "SHIFT-JIS", _("Japanese") ], + [False, "TCVN", _("Vietnamese") ], + [False, "TIS-620", _("Thai") ], + [False, "UHC", _("Korean") ], + [False, "VISCII", _("Vietnamese") ], + [False, "WINDOWS-1250", _("Central European") ], + [False, "WINDOWS-1251", _("Cyrillic") ], + [False, "WINDOWS-1252", _("Western") ], + [False, "WINDOWS-1253", _("Greek") ], + [False, "WINDOWS-1254", _("Turkish") ], + [False, "WINDOWS-1255", _("Hebrew") ], + [False, "WINDOWS-1256", _("Arabic") ], + [False, "WINDOWS-1257", _("Baltic") ], + [False, "WINDOWS-1258", _("Vietnamese") ] + ] - def __init__(self): - pass + def __init__(self): + pass - def get_list(): - """Return a list of supported encodings""" - return TerminatorEncoding.encodings - get_list = staticmethod(get_list) + def get_list(): + """Return a list of supported encodings""" + return TerminatorEncoding.encodings + + get_list = staticmethod(get_list) diff --git a/terminatorlib/factory.py b/terminatorlib/factory.py new file mode 100755 index 00000000..24442eeb --- /dev/null +++ b/terminatorlib/factory.py @@ -0,0 +1,97 @@ +#!/usr/bin/python +# Terminator by Chris Jones +# GPL v2 only +"""factory.py - Maker of objects + +>>> maker = Factory() +>>> window = maker.make_window() +>>> maker.isinstance(window, 'Window') +True +>>> terminal = maker.make_terminal() +>>> maker.isinstance(terminal, 'Terminal') +True +>>> hpaned = maker.make_hpaned() +>>> maker.isinstance(hpaned, 'HPaned') +True +>>> vpaned = maker.make_vpaned() +>>> maker.isinstance(vpaned, 'VPaned') +True + +""" + +from borg import Borg +from util import dbg, err + +# pylint: disable-msg=R0201 +# pylint: disable-msg=W0613 +class Factory(Borg): + """Definition of a class that makes other classes""" + def __init__(self): + """Class initialiser""" + Borg.__init__(self, self.__class__.__name__) + self.prepare_attributes() + + def prepare_attributes(self): + """Required by the borg, but a no-op here""" + pass + + def isinstance(self, product, classtype): + """Check if a given product is a particular type of object""" + types = {'Terminal': 'terminal', + 'VPaned': 'paned', + 'HPaned': 'paned', + 'Paned': 'paned', + 'Notebook': 'notebook', + 'Container': 'container', + 'Window': 'window'} + if classtype in types.keys(): + # This is quite ugly, but we're importing from the current + # directory if that makes sense, otherwise falling back to + # terminatorlib. Someone with real Python skills should fix + # this to be less insane. + try: + module = __import__(types[classtype], None, None, ['']) + except ImportError, ex: + module = __import__('terminatorlib.%s' % types[classtype], + None, None, ['']) + return(isinstance(product, getattr(module, classtype))) + else: + err('Factory::isinstance: unknown class type: %s' % classtype) + return(False) + + def make(self, product, **kwargs): + """Make the requested product""" + try: + func = getattr(self, 'make_%s' % product.lower()) + except AttributeError: + err('Factory::make: requested object does not exist: %s' % product) + return(None) + + dbg('Factory::make: created a %s' % product) + return(func(**kwargs)) + + def make_window(self, **kwargs): + """Make a Window""" + import window + return(window.Window(**kwargs)) + + def make_terminal(self, **kwargs): + """Make a Terminal""" + import terminal + return(terminal.Terminal()) + + def make_hpaned(self, **kwargs): + """Make an HPaned""" + import paned + return(paned.HPaned()) + + def make_vpaned(self, **kwargs): + """Make a VPaned""" + import paned + return(paned.VPaned()) + + def make_notebook(self, **kwargs): + """Make a Notebook""" + import notebook + return(notebook.Notebook(kwargs['window'])) + diff --git a/terminatorlib/keybindings.py b/terminatorlib/keybindings.py index 97ab6628..4cc1b870 100644 --- a/terminatorlib/keybindings.py +++ b/terminatorlib/keybindings.py @@ -1,6 +1,6 @@ #!/usr/bin/python -# Terminator - multiple gnome terminals in one window -# Copyright (C) 2006-2008 cmsj@tenshu.net +# Terminator - multiple gnome terminals in one window +# Copyright (C) 2006-2010 cmsj@tenshu.net # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -24,111 +24,111 @@ keyboard shortcuts. import re import gtk -from terminatorlib.config import err +from util import err class KeymapError(Exception): - """Custom exception for errors in keybinding configurations""" - def __init__(self, value): - Exception.__init__(self, value) - self.value = value - self.action = 'unknown' + """Custom exception for errors in keybinding configurations""" + def __init__(self, value): + Exception.__init__(self, value) + self.value = value + self.action = 'unknown' - def __str__(self): - return "Keybinding '%s' invalid: %s" % (self.action, self.value) + def __str__(self): + return "Keybinding '%s' invalid: %s" % (self.action, self.value) MODIFIER = re.compile('<([^<]+)>') -class TerminatorKeybindings: - """Class to handle loading and lookup of Terminator keybindings""" +class Keybindings: + """Class to handle loading and lookup of Terminator keybindings""" - modifiers = { - 'ctrl': gtk.gdk.CONTROL_MASK, - 'control': gtk.gdk.CONTROL_MASK, - 'shift': gtk.gdk.SHIFT_MASK, - 'alt': gtk.gdk.MOD1_MASK, - 'super': gtk.gdk.SUPER_MASK, - } + modifiers = { + 'ctrl': gtk.gdk.CONTROL_MASK, + 'control': gtk.gdk.CONTROL_MASK, + 'shift': gtk.gdk.SHIFT_MASK, + 'alt': gtk.gdk.MOD1_MASK, + 'super': gtk.gdk.SUPER_MASK, + } - empty = {} - keys = None - _masks = None - _lookup = None + empty = {} + keys = None + _masks = None + _lookup = None - def __init__(self): - self.keymap = gtk.gdk.keymap_get_default() - self.configure({}) + def __init__(self): + self.keymap = gtk.gdk.keymap_get_default() + self.configure({}) - def configure(self, bindings): - """Accept new bindings and reconfigure with them""" - self.keys = bindings - self.reload() + def configure(self, bindings): + """Accept new bindings and reconfigure with them""" + self.keys = bindings + self.reload() - def reload(self): - """Parse bindings and mangle into an appropriate form""" - self._lookup = {} - self._masks = 0 - for action, bindings in self.keys.items(): - if not isinstance(bindings, tuple): - bindings = (bindings,) + def reload(self): + """Parse bindings and mangle into an appropriate form""" + self._lookup = {} + self._masks = 0 + for action, bindings in self.keys.items(): + if not isinstance(bindings, tuple): + bindings = (bindings,) - for binding in bindings: - if binding is None or binding == "None": - continue + for binding in bindings: + if binding is None or binding == "None": + continue + try: + keyval, mask = self._parsebinding(binding) + # Does much the same, but with poorer error handling. + #keyval, mask = gtk.accelerator_parse(binding) + except KeymapError, ex: + continue + else: + if mask & gtk.gdk.SHIFT_MASK: + if keyval == gtk.keysyms.Tab: + keyval = gtk.keysyms.ISO_Left_Tab + mask &= ~gtk.gdk.SHIFT_MASK + else: + keyvals = gtk.gdk.keyval_convert_case(keyval) + if keyvals[0] != keyvals[1]: + keyval = keyvals[1] + mask &= ~gtk.gdk.SHIFT_MASK + else: + keyval = gtk.gdk.keyval_to_lower(keyval) + self._lookup.setdefault(mask, {}) + self._lookup[mask][keyval] = action + self._masks |= mask + + def _parsebinding(self, binding): + """Parse an individual binding using gtk's binding function""" + mask = 0 + modifiers = re.findall(MODIFIER, binding) + if modifiers: + for modifier in modifiers: + mask |= self._lookup_modifier(modifier) + key = re.sub(MODIFIER, '', binding) + if key == '': + raise KeymapError('No key found') + keyval = gtk.gdk.keyval_from_name(key) + if keyval == 0: + raise KeymapError("Key '%s' is unrecognised" % key) + return (keyval, mask) + + def _lookup_modifier(self, modifier): + """Map modifier names to gtk values""" try: - keyval, mask = self._parsebinding(binding) - # Does much the same, but with poorer error handling. - #keyval, mask = gtk.accelerator_parse(binding) - except KeymapError, ex: - continue - else: - if mask & gtk.gdk.SHIFT_MASK: - if keyval == gtk.keysyms.Tab: - keyval = gtk.keysyms.ISO_Left_Tab - mask &= ~gtk.gdk.SHIFT_MASK - else: - keyvals = gtk.gdk.keyval_convert_case(keyval) - if keyvals[0] != keyvals[1]: - keyval = keyvals[1] - mask &= ~gtk.gdk.SHIFT_MASK - else: - keyval = gtk.gdk.keyval_to_lower(keyval) - self._lookup.setdefault(mask, {}) - self._lookup[mask][keyval] = action - self._masks |= mask + return self.modifiers[modifier.lower()] + except KeyError: + raise KeymapError("Unhandled modifier '<%s>'" % modifier) - def _parsebinding(self, binding): - """Parse an individual binding using gtk's binding function""" - mask = 0 - modifiers = re.findall(MODIFIER, binding) - if modifiers: - for modifier in modifiers: - mask |= self._lookup_modifier(modifier) - key = re.sub(MODIFIER, '', binding) - if key == '': - raise KeymapError('No key found') - keyval = gtk.gdk.keyval_from_name(key) - if keyval == 0: - raise KeymapError("Key '%s' is unrecognised" % key) - return (keyval, mask) - - def _lookup_modifier(self, modifier): - """Map modifier names to gtk values""" - try: - return self.modifiers[modifier.lower()] - except KeyError: - raise KeymapError("Unhandled modifier '<%s>'" % modifier) - - def lookup(self, event): - """Translate a keyboard event into a mapped key""" - try: - keyval, egroup, level, consumed = self.keymap.translate_keyboard_state( - event.hardware_keycode, - event.state & ~gtk.gdk.LOCK_MASK, - event.group) - except TypeError: - err ("keybindings.lookup failed to translate keyboard event: %s" % - dir(event)) - return None - mask = (event.state & ~consumed) & self._masks - return self._lookup.get(mask, self.empty).get(keyval, None) + def lookup(self, event): + """Translate a keyboard event into a mapped key""" + try: + keyval, egroup, level, consumed = self.keymap.translate_keyboard_state( + event.hardware_keycode, + event.state & ~gtk.gdk.LOCK_MASK, + event.group) + except TypeError: + err ("keybindings.lookup failed to translate keyboard event: %s" % + dir(event)) + return None + mask = (event.state & ~consumed) & self._masks + return self._lookup.get(mask, self.empty).get(keyval, None) diff --git a/terminatorlib/notebook.py b/terminatorlib/notebook.py new file mode 100755 index 00000000..76121a3e --- /dev/null +++ b/terminatorlib/notebook.py @@ -0,0 +1,360 @@ +#!/usr/bin/python +# Terminator by Chris Jones +# GPL v2 only +"""notebook.py - classes for the notebook widget""" + +import gobject +import gtk + +from terminator import Terminator +from config import Config +from factory import Factory +from container import Container +from editablelabel import EditableLabel +from translation import _ +from util import err, dbg, get_top_window + +class Notebook(Container, gtk.Notebook): + """Class implementing a gtk.Notebook container""" + window = None + + def __init__(self, window): + """Class initialiser""" + if isinstance(window.get_child(), gtk.Notebook): + err('There is already a Notebook at the top of this window') + raise(ValueError) + + Container.__init__(self) + gtk.Notebook.__init__(self) + self.terminator = Terminator() + self.window = window + gobject.type_register(Notebook) + self.register_signals(Notebook) + self.configure() + + child = window.get_child() + window.remove(child) + window.add(self) + self.newtab(child) + + self.show_all() + + def configure(self): + """Apply widget-wide settings""" + # FIXME: Should all of our widgets have a ::configure()? + + # FIXME: The old reordered handler updated Terminator.terminals with + # the new order of terminals. We probably need to preserve this for + # navigation to next/prev terminals. + + #self.connect('page-reordered', self.on_page_reordered) + self.set_property('homogeneous', not self.config['scroll_tabbar']) + self.set_scrollable(self.config['scroll_tabbar']) + + pos = getattr(gtk, 'POS_%s' % self.config['tab_position'].upper()) + self.set_tab_pos(pos) + self.set_show_tabs(not self.config['hide_tabbar']) + + def split_axis(self, widget, vertical=True, sibling=None): + """Split the axis of a terminal inside us""" + page_num = self.page_num(widget) + if page_num == -1: + err('Notebook::split_axis: %s not found in Notebook' % widget) + return + + label = self.get_tab_label(widget) + self.remove(widget) + + maker = Factory() + if vertical: + container = maker.make('vpaned') + else: + container = maker.make('hpaned') + + if not sibling: + sibling = maker.make('terminal') + sibling.spawn_child() + + self.insert_page(container, None, page_num) + self.set_tab_label(container, label) + self.show_all() + + container.add(widget) + container.add(sibling) + self.set_current_page(page_num) + + self.show_all() + + def add(self, widget): + """Add a widget to the container""" + self.newtab(widget) + + def remove(self, widget): + """Remove a widget from the container""" + page_num = self.page_num(widget) + if page_num == -1: + err('Notebook::remove: %s not found in Notebook. Actual parent is: %s' % + (widget, widget.get_parent())) + return(False) + self.remove_page(page_num) + self.disconnect_child(widget) + return(True) + + def newtab(self, widget=None): + """Add a new tab, optionally supplying a child widget""" + top_window = get_top_window(self) + + if not widget: + maker = Factory() + widget = maker.make('Terminal') + widget.spawn_child() + + signals = {'close-term': self.wrapcloseterm, + 'split-horiz': self.split_horiz, + 'split-vert': self.split_vert, + 'title-change': self.propagate_title_change, + 'unzoom': self.unzoom} + + maker = Factory() + if maker.isinstance(widget, 'Terminal'): + for signal in signals: + self.connect_child(widget, signal, signals[signal]) + self.connect_child(widget, 'tab-change', top_window.tab_change) + self.connect_child(widget, 'group-all', top_window.group_all) + self.connect_child(widget, 'ungroup-all', top_window.ungroup_all) + self.connect_child(widget, 'group-tab', top_window.group_tab) + self.connect_child(widget, 'ungroup-tab', top_window.ungroup_tab) + + self.set_tab_reorderable(widget, True) + label = TabLabel(self.window.get_title(), self) + label.connect('close-clicked', self.closetab) + + label.show_all() + widget.show_all() + + self.append_page(widget, None) + self.set_tab_label(widget, label) + self.set_tab_label_packing(widget, not self.config['scroll_tabbar'], + not self.config['scroll_tabbar'], + gtk.PACK_START) + + self.set_current_page(-1) + widget.grab_focus() + + def wrapcloseterm(self, widget): + """A child terminal has closed""" + dbg('Notebook::wrapcloseterm: called on %s' % widget) + if self.closeterm(widget): + dbg('Notebook::wrapcloseterm: closeterm succeeded') + self.hoover() + else: + dbg('Notebook::wrapcloseterm: closeterm failed') + + def closetab(self, widget, label): + """Close a tab""" + tabnum = None + try: + nb = widget.notebook + except AttributeError: + err('TabLabel::closetab: called on non-Notebook: %s' % widget) + return + + for i in xrange(0, nb.get_n_pages()): + if label == nb.get_tab_label(nb.get_nth_page(i)): + tabnum = i + break + + if not tabnum: + err('TabLabel::closetab: %s not in %s. Bailing.' % (label, nb)) + return + + maker = Factory() + child = nb.get_nth_page(tabnum) + + if maker.isinstance(child, 'Terminal'): + dbg('Notebook::closetab: child is a single Terminal') + child.close() + elif maker.isinstance(child, 'Container'): + dbg('Notebook::closetab: child is a Container') + dialog = self.construct_confirm_close(self.window, _('tab')) + result = dialog.run() + dialog.destroy() + + if result == gtk.RESPONSE_ACCEPT: + containers = [] + objects = [] + for descendant in child.get_children(): + if maker.isinstance(descendant, 'Container'): + containers.append(descendant) + elif maker.isinstance(descendant, 'Terminal'): + objects.append(descendant) + + while len(containers) > 0: + child = containers.pop() + for descendant in child.get_children(): + if maker.isinstance(descendant, 'Container'): + containers.append(descendant) + elif maker.isinstance(descendant, 'Terminal'): + objects.append(descendant) + + while len(objects) > 0: + descendant = objects.pop() + descendant.close() + # FIXME: Is this mainloop iterations stuff necessary? + while gtk.events_pending(): + gtk.main_iteration() + return + else: + dbg('Notebook::closetab: user cancelled request') + return + else: + err('Notebook::closetab: child is unknown type %s' % child) + return + + nb.remove_page(tabnum) + del(label) + + def resizeterm(self, widget, keyname): + """Handle a keyboard event requesting a terminal resize""" + raise NotImplementedError('resizeterm') + + def zoom(self, widget, fontscale = False): + """Zoom a terminal""" + raise NotImplementedError('zoom') + + def unzoom(self, widget): + """Unzoom a terminal""" + raise NotImplementedError('unzoom') + + def find_tab_root(self, widget): + """Look for the tab child which is or ultimately contains the supplied + widget""" + parent = widget.get_parent() + previous = parent + + while parent is not None and parent is not self: + previous = parent + parent = parent.get_parent() + + if previous == self: + return(widget) + else: + return(previous) + + def update_tab_label_text(self, widget, text): + """Update the text of a tab label""" + notebook = self.find_tab_root(widget) + label = self.get_tab_label(notebook) + if not label: + err('Notebook::update_tab_label_text: %s not found' % widget) + return + + label.set_label(text) + + def hoover(self): + """Clean up any empty tabs and if we only have one tab left, die""" + numpages = self.get_n_pages() + while numpages > 0: + numpages = numpages - 1 + page = self.get_nth_page(numpages) + if not page: + dbg('Removing empty page: %d' % numpages) + self.remove_page(numpages) + + if self.get_n_pages() == 1: + dbg('Last page, removing self') + child = self.get_nth_page(0) + self.remove_page(0) + parent = self.get_parent() + parent.remove(self) + parent.add(child) + del(self) + +class TabLabel(gtk.HBox): + """Class implementing a label widget for Notebook tabs""" + notebook = None + terminator = None + config = None + label = None + icon = None + button = None + + __gsignals__ = { + 'close-clicked': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, + (gobject.TYPE_OBJECT,)), + } + + def __init__(self, title, notebook): + """Class initialiser""" + gtk.HBox.__init__(self) + self.__gobject_init__() + + self.notebook = notebook + self.terminator = Terminator() + self.config = Config() + + self.label = EditableLabel(title) + self.update_angle() + + self.pack_start(self.label, True, True) + + self.update_button() + self.show_all() + + def set_label(self, text): + """Update the text of our label""" + self.label.set_text(text) + + def update_button(self): + """Update the state of our close button""" + if not self.config['close_button_on_tab']: + if self.button: + self.button.remove(self.icon) + self.remove(self.button) + del(self.button) + del(self.icon) + self.button = None + self.icon = None + return + + if not self.button: + self.button = gtk.Button() + if not self.icon: + self.icon = gtk.Image() + self.icon.set_from_stock(gtk.STOCK_CLOSE, + gtk.ICON_SIZE_MENU) + + self.button.set_relief(gtk.RELIEF_NONE) + self.button.set_focus_on_click(False) + # FIXME: Why on earth are we doing this twice? + self.button.set_relief(gtk.RELIEF_NONE) + self.button.add(self.icon) + self.button.connect('clicked', self.on_close) + self.button.set_name('terminator-tab-close-button') + self.button.connect('style-set', self.on_style_set) + if hasattr(self.button, 'set_tooltip_text'): + self.button.set_tooltip_text(_('Close Tab')) + self.pack_start(self.button, False, False) + self.show_all() + + def update_angle(self): + """Update the angle of a label""" + position = self.notebook.get_tab_pos() + if position == gtk.POS_LEFT: + self.label.set_angle(90) + elif position == gtk.POS_RIGHT: + self.label.set_angle(270) + else: + self.label.set_angle(0) + + def on_style_set(self, widget, prevstyle): + """Style changed, recalculate icon size""" + x, y = gtk.icon_size_lookup_for_settings(self.button.get_settings(), + gtk.ICON_SIZE_MENU) + self.button.set_size_request(x + 2, y + 2) + + def on_close(self, widget): + """The close button has been clicked. Destroy the tab""" + self.emit('close-clicked', self) + +# vim: set expandtab ts=4 sw=4: diff --git a/terminatorlib/optionparse.py b/terminatorlib/optionparse.py new file mode 100755 index 00000000..8ad7899b --- /dev/null +++ b/terminatorlib/optionparse.py @@ -0,0 +1,101 @@ +#!/usr/bin/python +# Terminator.optionparse - Parse commandline options +# Copyright (C) 2006-2010 cmsj@tenshu.net +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 2 only. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +"""Terminator.optionparse - Parse commandline options""" + +import sys +import os + +from optparse import OptionParser, SUPPRESS_HELP +from util import dbg, err +import util +import config +import version + +def execute_cb(option, opt, value, lparser): + """Callback for use in parsing execute options""" + assert value is None + value = [] + while lparser.rargs: + arg = lparser.rargs[0] + value.append(arg) + del(lparser.rargs[0]) + setattr(lparser.values, option.dest, value) + +def parse_options(): + """Parse the command line options""" + usage = "usage: %prog [options]" + + configobj = config.Config() + parser = OptionParser(usage) + + parser.add_option('-v', '--version', action='store_true', dest='version', + help='Display program version') + parser.add_option('-d', '--debug', action='count', dest='debug', + help='Enable debugging information (twice for debug server)') + parser.add_option('-m', '--maximise', action='store_true', dest='maximise', + help='Maximise the window') + parser.add_option('-f', '--fullscreen', action='store_true', + dest='fullscreen', help='Make the window fill the screen') + parser.add_option('-b', '--borderless', action='store_true', + dest='borderless', help='Disable window borders') + parser.add_option('-H', '--hidden', action='store_true', dest='hidden', + help='Hide the window at startup') + parser.add_option('-T', '--title', dest='forcedtitle', help='Specify a \ +title for the window') + parser.add_option('--geometry', dest='geometry', type='string', help='Set \ +the preferred size and position of the window (see X man page)') + parser.add_option('-e', '--command', dest='command', help='Specify a \ +command to execute inside the terminal') + parser.add_option('-x', '--execute', dest='execute', action='callback', + callback=execute_cb, help='Use the rest of the command line as a \ +command to execute inside the terminal, and its arguments') + parser.add_option('--working-directory', metavar='DIR', + dest='working_directory', help='Set the working directory') + parser.add_option('-r', '--role', dest='role', help='Set a custom \ +WM_WINDOW_ROLE property on the window') + for item in ['--sm-client-id', '--sm-config-prefix', '--screen', '-n', + '--no-gconf', '-p', '--profile' ]: + parser.add_option(item, dest='dummy', action='store', + help=SUPPRESS_HELP) + + (options, args) = parser.parse_args() + if len(args) != 0: + parser.error('Additional unexpected arguments found: %s' % args) + + if options.version: + print '%s %s' % (version.APP_NAME, version.APP_VERSION) + sys.exit(0) + + if options.debug: + util.DEBUG = True + if options.debug >1: + util.DEBUGFILES = True + + if options.working_directory: + if os.path.exists(os.path.expanduser(options.working_directory)): + os.chdir(os.path.expanduser(options.working_directory)) + else: + err('OptionParse::parse_options: %s does not exist' % + options.working_directory) + sys.exit(1) + + configobj.options_set(options) + + if util.DEBUG == True: + dbg('OptionParse::parse_options: command line options: %s' % options) + + return(options) diff --git a/terminatorlib/paned.py b/terminatorlib/paned.py new file mode 100755 index 00000000..42670ad5 --- /dev/null +++ b/terminatorlib/paned.py @@ -0,0 +1,195 @@ +#!/usr/bin/python +# Terminator by Chris Jones +# GPL v2 only +"""paned.py - a base Paned container class and the vertical/horizontal +variants""" + +import gobject +import gtk + +from util import dbg, err, get_top_window +from terminator import Terminator +from factory import Factory +from container import Container + +# pylint: disable-msg=R0921 +# pylint: disable-msg=E1101 +class Paned(Container): + """Base class for Paned Containers""" + + def __init__(self): + """Class initialiser""" + self.terminator = Terminator() + Container.__init__(self) + self.signals.append({'name': 'resize-term', + 'flags': gobject.SIGNAL_RUN_LAST, + 'return_type': gobject.TYPE_NONE, + 'param_types': (gobject.TYPE_STRING,)}) + + + # pylint: disable-msg=W0613 + def set_initial_position(self, widget, event): + """Set the initial position of the widget""" + if isinstance(self, gtk.VPaned): + position = self.allocation.height / 2 + else: + position = self.allocation.width / 2 + + dbg("Paned::set_initial_position: Setting position to: %d" % position) + self.set_position(position) + self.cnxids.remove_signal(self, 'expose-event') + + # pylint: disable-msg=W0613 + def split_axis(self, widget, vertical=True, sibling=None): + """Default axis splitter. This should be implemented by subclasses""" + maker = Factory() + + self.remove(widget) + if vertical: + container = VPaned() + else: + container = HPaned() + + if not sibling: + sibling = maker.make('terminal') + sibling.spawn_child() + + self.add(container) + self.show_all() + + container.add(widget) + container.add(sibling) + + self.show_all() + + def add(self, widget): + """Add a widget to the container""" + maker = Factory() + if len(self.children) == 0: + self.pack1(widget, True, True) + self.children.append(widget) + elif len(self.children) == 1: + if self.get_child1(): + self.pack2(widget, True, True) + else: + self.pack1(widget, True, True) + self.children.append(widget) + else: + raise ValueError('Paned widgets can only have two children') + + if maker.isinstance(widget, 'Terminal'): + top_window = get_top_window(self) + signals = {'close-term': self.wrapcloseterm, + 'split-horiz': self.split_horiz, + 'split-vert': self.split_vert, + 'title-change': self.propagate_title_change, + 'resize-term': self.resizeterm, + 'zoom': top_window.zoom} + + for signal in signals: + self.connect_child(widget, signal, signals[signal]) + + # FIXME: We shouldn't be doing this exact same thing in each + # Container + self.connect_child(widget, 'maximise', top_window.zoom, False) + self.connect_child(widget, 'tab-change', top_window.tab_change) + self.connect_child(widget, 'group-all', top_window.group_all) + self.connect_child(widget, 'ungroup-all', top_window.ungroup_all) + self.connect_child(widget, 'group-tab', top_window.group_tab) + self.connect_child(widget, 'ungroup-tab', top_window.ungroup_tab) + + widget.grab_focus() + + elif isinstance(widget, gtk.Paned): + try: + self.connect_child(widget, 'resize-term', self.resizeterm) + except TypeError: + err('Paned::add: %s has no signal resize-term' % widget) + + def remove(self, widget): + """Remove a widget from the container""" + gtk.Paned.remove(self, widget) + self.disconnect_child(widget) + self.children.remove(widget) + return(True) + + def wrapcloseterm(self, widget): + """A child terminal has closed, so this container must die""" + dbg('Paned::wrapcloseterm: Called on %s' % widget) + if self.closeterm(widget): + # At this point we only have one child, which is the surviving term + sibling = self.children[0] + self.remove(sibling) + + parent = self.get_parent() + parent.remove(self) + parent.add(sibling) + del(self) + else: + dbg("Paned::wrapcloseterm: self.closeterm failed") + + def hoover(self): + """Check that we still have a reason to exist""" + if len(self.children) == 1: + dbg('Paned::hoover: We only have one child, die') + parent = self.get_parent() + parent.remove(self) + child = self.children[0] + self.remove(child) + parent.add(child) + del(self) + + def resizeterm(self, widget, keyname): + """Handle a keyboard event requesting a terminal resize""" + maker = Factory() + if keyname in ['up', 'down'] and isinstance(self, gtk.VPaned): + # This is a key we can handle + position = self.get_position() + + if maker.isinstance(widget, 'Terminal'): + fontheight = widget.vte.get_char_height() + else: + fontheight = 10 + + if keyname == 'up': + self.set_position(position - fontheight) + else: + self.set_position(position + fontheight) + elif keyname in ['left', 'right'] and isinstance(self, gtk.HPaned): + # This is a key we can handle + position = self.get_position() + + if maker.isinstance(widget, 'Terminal'): + fontwidth = widget.vte.get_char_width() + else: + fontwidth = 10 + + if keyname == 'left': + self.set_position(position - fontwidth) + else: + self.set_position(position + fontwidth) + else: + # This is not a key we can handle + self.emit('resize-term', keyname) + +class HPaned(Paned, gtk.HPaned): + """Merge gtk.HPaned into our base Paned Container""" + def __init__(self): + """Class initialiser""" + Paned.__init__(self) + gtk.HPaned.__init__(self) + self.register_signals(HPaned) + self.cnxids.new(self, 'expose-event', self.set_initial_position) + +class VPaned(Paned, gtk.VPaned): + """Merge gtk.VPaned into our base Paned Container""" + def __init__(self): + """Class initialiser""" + Paned.__init__(self) + gtk.VPaned.__init__(self) + self.register_signals(VPaned) + self.cnxids.new(self, 'expose-event', self.set_initial_position) + +gobject.type_register(HPaned) +gobject.type_register(VPaned) +# vim: set expandtab ts=4 sw=4: diff --git a/terminatorlib/plugin.py b/terminatorlib/plugin.py new file mode 100755 index 00000000..1731f780 --- /dev/null +++ b/terminatorlib/plugin.py @@ -0,0 +1,143 @@ +#!/usr/bin/python +# Terminator by Chris Jones +# GPL v2 only +"""plugin.py - Base plugin system + Inspired by Armin Ronacher's post at + http://lucumr.pocoo.org/2006/7/3/python-plugin-system + Used with permission (the code in that post is to be + considered BSD licenced, per the authors wishes) + +>>> registry = PluginRegistry() +>>> registry.instances +{} +>>> registry.load_plugins(True) +>>> plugins = registry.get_plugins_by_capability('test') +>>> len(plugins) +1 +>>> plugins[0] #doctest: +ELLIPSIS + +>>> registry.get_plugins_by_capability('this_should_not_ever_exist') +[] +>>> plugins[0].do_test() +'TestPluginWin' + +""" + +import sys +import os +import borg +from config import Config +from util import dbg, err, get_config_dir + +class Plugin(object): + """Definition of our base plugin class""" + capabilities = None + + def __init__(self): + """Class initialiser.""" + pass + +class PluginRegistry(borg.Borg): + """Definition of a class to store plugin instances""" + instances = None + path = None + done = None + + def __init__(self): + """Class initialiser""" + borg.Borg.__init__(self, self.__class__.__name__) + self.prepare_attributes() + + def prepare_attributes(self): + """Prepare our attributes""" + if not self.instances: + self.instances = {} + if not self.path: + self.path = [] + (head, tail) = os.path.split(borg.__file__) + self.path.append(os.path.join(head, 'plugins')) + self.path.append(os.path.join(get_config_dir(), 'plugins')) + dbg('PluginRegistry::prepare_attributes: Plugin path: %s' % + self.path) + if not self.done: + self.done = False + + def load_plugins(self, testing=False): + """Load all plugins present in the plugins/ directory in our module""" + if self.done: + dbg('PluginRegistry::load_plugins: Already loaded') + return + + config = Config() + + for plugindir in self.path: + sys.path.insert(0, plugindir) + try: + files = os.listdir(plugindir) + except OSError: + sys.path.remove(plugindir) + continue + for plugin in files: + pluginpath = os.path.join(plugindir, plugin) + if os.path.isfile(pluginpath) and plugin[-3:] == '.py': + dbg('PluginRegistry::load_plugins: Importing plugin %s' % + plugin) + try: + module = __import__(plugin[:-3], None, None, ['']) + for item in getattr(module, 'available'): + if not testing and item in config['disabled_plugins']: + continue + if item not in self.instances: + func = getattr(module, item) + self.instances[item] = func() + except Exception, e: + err('PluginRegistry::load_plugins: Importing plugin %s \ +failed: %s' % (plugin, e)) + + self.done = True + + def get_plugins_by_capability(self, capability): + """Return a list of plugins with a particular capability""" + result = [] + dbg('PluginRegistry::get_plugins_by_capability: searching %d plugins \ +for %s' % (len(self.instances), capability)) + for plugin in self.instances: + if capability in self.instances[plugin].capabilities: + result.append(self.instances[plugin]) + return result + + def get_all_plugins(self): + """Return all plugins""" + return(self.instances) + +# This is where we should define a base class for each type of plugin we +# support + +# URLHandler - This adds a regex match to the Terminal widget and provides a +# callback to turn that into a URL. +class URLHandler(Plugin): + """Base class for URL handlers""" + capabilities = ['url_handler'] + handler_name = None + match = None + + def callback(self, url): + """Callback to transform the enclosed URL""" + raise NotImplementedError + +# MenuItem - This is able to execute code during the construction of the +# context menu of a Terminal. +class MenuItem(Plugin): + """Base class for menu items""" + capabilities = ['terminal_menu'] + + def callback(self, menuitems, menu, terminal): + """Callback to transform the enclosed URL""" + raise NotImplementedError + +if __name__ == '__main__': + import doctest + sys.path.insert(0, 'plugins') + (failed, attempted) = doctest.testmod() + print "%d/%d tests failed" % (failed, attempted) + sys.exit(failed) diff --git a/terminatorlib/plugins/custom_commands.py b/terminatorlib/plugins/custom_commands.py new file mode 100755 index 00000000..5fbe31a1 --- /dev/null +++ b/terminatorlib/plugins/custom_commands.py @@ -0,0 +1,434 @@ +#!/usr/bin/python +# Terminator by Chris Jones +# GPL v2 only +"""custom_commands.py - Terminator Plugin to add custom command menu entries""" +import sys +import os + +# Fix imports when testing this file directly +if __name__ == '__main__': + sys.path.append( os.path.join(os.path.dirname(__file__), "../..")) + +import gtk +import terminatorlib.plugin as plugin +from terminatorlib.config import Config +from terminatorlib.translation import _ +from terminatorlib.util import get_config_dir + +(CC_COL_ENABLED, CC_COL_NAME, CC_COL_COMMAND) = range(0,3) + +# Every plugin you want Terminator to load *must* be listed in 'available' +available = ['CustomCommandsMenu'] + +class CustomCommandsMenu(plugin.MenuItem): + """Add custom commands to the terminal menu""" + capabilities = ['terminal_menu'] + cmd_list = [] + conf_file = os.path.join(get_config_dir(),"custom_commands") + + def __init__( self): + config = Config() + sections = config.plugin_get_config(self.__class__.__name__) + if not isinstance(sections, dict): + return + for part in sections: + s = sections[part] + if not (s.has_key("name") and s.has_key("command")): + print "CustomCommandsMenu: Ignoring section %s" % s + continue + name = s["name"] + command = s["command"] + enabled = s["enabled"] and s["enabled"] or False + self.cmd_list.append( + {'enabled' : enabled, + 'name' : name, + 'command' : command + } + ) + def callback(self, menuitems, menu, terminal): + """Add our menu items to the menu""" + item = gtk.MenuItem(_('Custom Commands')) + menuitems.append(item) + + submenu = gtk.Menu() + item.set_submenu(submenu) + + menuitem = gtk.ImageMenuItem(gtk.STOCK_PREFERENCES) + menuitem.connect("activate", self.configure) + submenu.append(menuitem) + + menuitem = gtk.SeparatorMenuItem() + submenu.append(menuitem) + + theme = gtk.IconTheme() + for command in self.cmd_list: + if not command['enabled']: + continue + exe = command['command'].split(' ')[0] + iconinfo = theme.choose_icon([exe], gtk.ICON_SIZE_MENU, gtk.ICON_LOOKUP_USE_BUILTIN) + if iconinfo: + image = gtk.Image() + image.set_from_icon_name(exe, gtk.ICON_SIZE_MENU) + menuitem = gtk.ImageMenuItem(command['name']) + menuitem.set_image(image) + else: + menuitem = gtk.MenuItem(command["name"]) + menuitem.connect("activate", self._execute, {'terminal' : terminal, 'command' : command['command'] }) + submenu.append(menuitem) + + def _save_config(self): + config = Config() + i = 0 + length = len(self.cmd_list) + while i < length: + enabled = self.cmd_list[i]['enabled'] + name = self.cmd_list[i]['name'] + command = self.cmd_list[i]['command'] + + item = {} + item['enabled'] = enabled + item['name'] = name + item['command'] = command + + config.plugin_set(self.__class__.__name__, name, item) + config.save() + i = i + 1 + + def _execute(self, widget, data): + command = data['command'] + if command[len(command)-1] != '\n': + command = command + '\n' + data['terminal'].vte.feed_child(command) + + def configure(self, widget, data = None): + ui = {} + dbox = gtk.Dialog( + _("Custom Commands Configuration"), + None, + gtk.DIALOG_MODAL, + ( + gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, + gtk.STOCK_OK, gtk.RESPONSE_ACCEPT + ) + ) + store = gtk.ListStore(bool, str, str) + + for command in self.cmd_list: + store.append([command['enabled'], command['name'], command['command']]) + + treeview = gtk.TreeView(store) + #treeview.connect("cursor-changed", self.on_cursor_changed, ui) + selection = treeview.get_selection() + selection.set_mode(gtk.SELECTION_SINGLE) + selection.connect("changed", self.on_selection_changed, ui) + ui['treeview'] = treeview + + renderer = gtk.CellRendererToggle() + renderer.connect('toggled', self.on_toggled, ui) + column = gtk.TreeViewColumn("Enabled", renderer, active=CC_COL_ENABLED) + treeview.append_column(column) + + renderer = gtk.CellRendererText() + column = gtk.TreeViewColumn("Name", renderer, text=CC_COL_NAME) + treeview.append_column(column) + + renderer = gtk.CellRendererText() + column = gtk.TreeViewColumn("Command", renderer, text=CC_COL_COMMAND) + treeview.append_column(column) + + hbox = gtk.HBox() + hbox.pack_start(treeview) + dbox.vbox.pack_start(hbox) + + button_box = gtk.VBox() + + button = gtk.Button(stock=gtk.STOCK_GOTO_TOP) + button_box.pack_start(button, False, True) + button.connect("clicked", self.on_goto_top, ui) + button.set_sensitive(False) + ui['button_top'] = button + + button = gtk.Button(stock=gtk.STOCK_GO_UP) + button_box.pack_start(button, False, True) + button.connect("clicked", self.on_go_up, ui) + button.set_sensitive(False) + ui['button_up'] = button + + button = gtk.Button(stock=gtk.STOCK_GO_DOWN) + button_box.pack_start(button, False, True) + button.connect("clicked", self.on_go_down, ui) + button.set_sensitive(False) + ui['button_down'] = button + + button = gtk.Button(stock=gtk.STOCK_GOTO_LAST) + button_box.pack_start(button, False, True) + button.connect("clicked", self.on_goto_last, ui) + button.set_sensitive(False) + ui['button_last'] = button + + button = gtk.Button(stock=gtk.STOCK_NEW) + button_box.pack_start(button, False, True) + button.connect("clicked", self.on_new, ui) + ui['button_new'] = button + + button = gtk.Button(stock=gtk.STOCK_EDIT) + button_box.pack_start(button, False, True) + button.set_sensitive(False) + button.connect("clicked", self.on_edit, ui) + ui['button_edit'] = button + + button = gtk.Button(stock=gtk.STOCK_DELETE) + button_box.pack_start(button, False, True) + button.connect("clicked", self.on_delete, ui) + button.set_sensitive(False) + ui['button_delete'] = button + + + + hbox.pack_start(button_box) + dbox.show_all() + res = dbox.run() + if res == gtk.RESPONSE_ACCEPT: + #we save the config + iter = store.get_iter_first() + self.cmd_list = [] + while iter: + (enabled, name, command) = store.get(iter, + CC_COL_ENABLED, + CC_COL_NAME, + CC_COL_COMMAND) + self.cmd_list.append( + {'enabled' : enabled, + 'name': name, + 'command' : command} + ) + iter = store.iter_next(iter) + self._save_config() + + dbox.destroy() + return + + def on_toggled(self, widget, path, data): + treeview = data['treeview'] + store = treeview.get_model() + iter = store.get_iter(path) + (enabled, name, command) = store.get(iter, + CC_COL_ENABLED, + CC_COL_NAME, + CC_COL_COMMAND + ) + store.set_value(iter, CC_COL_ENABLED, not enabled) + for cmd in self.cmd_list: + if cmd['name'] == name: + cmd['enabled'] = not enabled + break + + def on_selection_changed(self,selection, data=None): + treeview = selection.get_tree_view() + (model, iter) = selection.get_selected() + data['button_top'].set_sensitive(iter is not None) + data['button_up'].set_sensitive(iter is not None) + data['button_down'].set_sensitive(iter is not None) + data['button_last'].set_sensitive(iter is not None) + data['button_edit'].set_sensitive(iter is not None) + data['button_delete'].set_sensitive(iter is not None) + + def _create_command_dialog(self, enabled_var = False, name_var = "", command_var = ""): + dialog = gtk.Dialog( + _("New Command"), + None, + gtk.DIALOG_MODAL, + ( + gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, + gtk.STOCK_OK, gtk.RESPONSE_ACCEPT + ) + ) + table = gtk.Table(3, 2) + + label = gtk.Label(_("Enabled:")) + table.attach(label, 0, 1, 0, 1) + enabled = gtk.CheckButton() + enabled.set_active(enabled_var) + table.attach(enabled, 1, 2, 0, 1) + + label = gtk.Label(_("Name:")) + table.attach(label, 0, 1, 1, 2) + name = gtk.Entry() + name.set_text(name_var) + table.attach(name, 1, 2, 1, 2) + + label = gtk.Label(_("Command:")) + table.attach(label, 0, 1, 2, 3) + command = gtk.Entry() + command.set_text(command_var) + table.attach(command, 1, 2, 2, 3) + + dialog.vbox.pack_start(table) + dialog.show_all() + return (dialog,enabled,name,command) + + def _error(self, msg): + err = gtk.MessageDialog(dialog, + gtk.DIALOG_MODAL, + gtk.MESSAGE_ERROR, + gtk.BUTTONS_CLOSE, + msg + ) + err.run() + err.destroy() + + + + + def on_new(self, button, data): + (dialog,enabled,name,command) = self._create_command_dialog() + res = dialog.run() + item = {} + if res == gtk.RESPONSE_ACCEPT: + item['enabled'] = enabled.get_active() + item['name'] = name.get_text() + item['command'] = command.get_text() + if item['name'] == '' or item['command'] == '': + err = gtk.MessageDialog(dialog, + gtk.DIALOG_MODAL, + gtk.MESSAGE_ERROR, + gtk.BUTTONS_CLOSE, + _("You need to define a name and command") + ) + err.run() + err.destroy() + else: + # we have a new command + store = data['treeview'].get_model() + iter = store.get_iter_first() + name_exist = False + while iter != None: + if store.get_value(iter,CC_COL_NAME) == item['name']: + name_exist = True + break + iter = store.iter_next(iter) + if not name_exist: + store.append((item['enabled'], item['name'], item['command'])) + else: + self._err(_("Name *%s* already exist") % item['name']) + dialog.destroy() + + def on_goto_top(self, button, data): + treeview = data['treeview'] + selection = treeview.get_selection() + (store, iter) = selection.get_selected() + + if not iter: + return + firstiter = store.get_iter_first() + store.move_before(iter, firstiter) + + def on_go_up(self, button, data): + treeview = data['treeview'] + selection = treeview.get_selection() + (store, iter) = selection.get_selected() + + if not iter: + return + + tmpiter = store.get_iter_first() + + if(store.get_path(tmpiter) == store.get_path(iter)): + return + + while tmpiter: + next = store.iter_next(tmpiter) + if(store.get_path(next) == store.get_path(iter)): + store.swap(iter, tmpiter) + break + tmpiter = next + + def on_go_down(self, button, data): + treeview = data['treeview'] + selection = treeview.get_selection() + (store, iter) = selection.get_selected() + + if not iter: + return + next = store.iter_next(iter) + if next: + store.swap(iter, next) + + def on_goto_last(self, button, data): + treeview = data['treeview'] + selection = treeview.get_selection() + (store, iter) = selection.get_selected() + + if not iter: + return + lastiter = iter + tmpiter = store.get_iter_first() + while tmpiter: + lastiter = tmpiter + tmpiter = store.iter_next(tmpiter) + + store.move_after(iter, lastiter) + + + def on_delete(self, button, data): + treeview = data['treeview'] + selection = treeview.get_selection() + (store, iter) = selection.get_selected() + if iter: + store.remove(iter) + + return + + def on_edit(self, button, data): + treeview = data['treeview'] + selection = treeview.get_selection() + (store, iter) = selection.get_selected() + + if not iter: + return + + (dialog,enabled,name,command) = self._create_command_dialog( + enabled_var = store.get_value(iter, CC_COL_ENABLED), + name_var = store.get_value(iter, CC_COL_NAME), + command_var = store.get_value(iter, CC_COL_COMMAND) + ) + res = dialog.run() + item = {} + if res == gtk.RESPONSE_ACCEPT: + item['enabled'] = enabled.get_active() + item['name'] = name.get_text() + item['command'] = command.get_text() + if item['name'] == '' or item['command'] == '': + err = gtk.MessageDialog(dialog, + gtk.DIALOG_MODAL, + gtk.MESSAGE_ERROR, + gtk.BUTTONS_CLOSE, + _("You need to define a name and command") + ) + err.run() + err.destroy() + else: + tmpiter = store.get_iter_first() + name_exist = False + while tmpiter != None: + if store.get_path(tmpiter) != store.get_path(iter) and store.get_value(tmpiter,CC_COL_NAME) == item['name']: + name_exist = True + break + tmpiter = store.iter_next(tmpiter) + if not name_exist: + store.set(iter, + CC_COL_ENABLED,item['enabled'], + CC_COL_NAME, item['name'], + CC_COL_COMMAND, item['command'] + ) + else: + self._err(_("Name *%s* already exist") % item['name']) + + dialog.destroy() + + +if __name__ == '__main__': + c = CustomCommandsMenu() + c.configure(None, None) + gtk.main() + diff --git a/terminatorlib/plugins/terminal_menu.py b/terminatorlib/plugins/terminal_menu.py new file mode 100644 index 00000000..93b5e62e --- /dev/null +++ b/terminatorlib/plugins/terminal_menu.py @@ -0,0 +1,21 @@ +# Terminator by Chris Jones + + + + + + + + + + + + + + + + + GNOME Default + + + Click to focus + + + Follow mouse pointer + + + + + + + + + + + Normal + + + Hidden + + + Maximised + + + Fullscreen + + + + + + + + + + + Top + + + Bottom + + + Left + + + Right + + + + + + + + + + + Block + + + Underline + + + I-Beam + + + + + + + + + + + Exit the terminal + + + Restart the command + + + Hold the terminal open + + + + + + + + + + + Black on light yellow + + + Black on white + + + Grey on black + + + Green on black + + + White on black + + + Orange on black + + + Custom + + + + + + + + + + + On the left side + + + On the right side + + + Disabled + + + + + + + + + + + Control-H + + + ASCII DEL + + + Escape sequence + + + + + + + + + + + Control-H + + + ASCII DEL + + + Escape sequence + + + + + + + + + + + + + + + + + 5 + normal + False + + + True + vertical + 2 + + + True + True + + + True + 6 + + + True + 6 + 2 + 6 + + + True + Window geometry hints + + + 2 + 3 + + + + + + + True + True + False + True + True + + + 1 + 2 + 2 + 3 + + GTK_EXPAND + + + + + True + Window state + + + 3 + 4 + + + + + + + True + WindowStateListStore + 0 + + + + 0 + + + + + 1 + 2 + 3 + 4 + + GTK_EXPAND + + + + + True + Window borders + + + 4 + 5 + + + + + + + True + True + False + True + True + + + 1 + 2 + 4 + 5 + + GTK_EXPAND + + + + + True + Tab position + + + 5 + 6 + + + + + + + True + TabPositionListStore + 0 + + + + 0 + + + + + 1 + 2 + 5 + 6 + + GTK_EXPAND + + + + + True + Mouse focus + + + + + + + + + True + FocusListStore + 0 + + + + 0 + + + + + 1 + 2 + + GTK_EXPAND + + + + + True + Terminal separator size + + + 1 + 2 + + + + + + + True + True + adjustment1 + 0 + left + + + 1 + 2 + 1 + 2 + GTK_EXPAND + 20 + + + + + + + + + True + Global + + + False + + + + + True + + + True + vertical + + + True + True + ProfilesListStore + adjustment2 + adjustment3 + False + 0 + + + Profile + True + + + True + + + + 0 + + + + + + + 0 + + + + + True + + + + + + gtk-add + True + True + True + True + + + + False + False + 1 + + + + + gtk-remove + True + True + True + True + + + + False + False + 2 + + + + + False + 1 + + + + + 0 + + + + + True + True + 5 + + + True + 12 + vertical + 6 + + + _Use the system fixed width font + True + True + False + True + True + + + + False + False + 0 + + + + + True + 12 + + + True + 12 + + + True + 0 + _Font: + True + font-selector + + + False + False + 0 + + + + + True + True + True + False + Choose A Terminal Font + True + True + + + False + False + 1 + + + + + + + False + False + 1 + + + + + _Allow bold text + True + True + False + True + True + + + False + False + 2 + + + + + Visual terminal _bell + True + True + False + True + True + + + False + False + 3 + + + + + Audible terminal _bell + True + True + False + True + True + + + False + False + 4 + + + + + WM_URGENT terminal _bell + True + True + False + True + True + + + False + False + 5 + + + + + True + 12 + + + True + Cursor _shape: + True + cursor-shape-combobox + + + False + False + 0 + + + + + True + CursorShapeListStore + 0 + + + + 0 + + + + + False + 1 + + + + + False + 6 + + + + + True + 12 + + + True + 0 + Select-by-_word characters: + True + center + word-chars-entry + + + False + False + 0 + + + + + True + True + + + + 1 + + + + + False + 7 + + + + + + + True + General + True + center + + + False + + + + + True + 6 + 6 + 6 + 6 + + + True + vertical + 6 + + + True + 0 + <b>Command</b> + True + + + False + False + 0 + + + + + True + 12 + + + True + vertical + + + True + vertical + 6 + + + _Run command as a login shell + True + True + False + True + True + + + False + False + 0 + + + + + _Update login records when command is launched + True + True + False + True + True + + + False + False + 1 + + + + + Ru_n a custom command instead of my shell + True + True + False + True + True + + + + False + False + 2 + + + + + True + 12 + + + True + 12 + + + True + 0 + Custom co_mmand: + True + center + custom-command-entry + + + False + False + 0 + + + + + True + True + + + + 1 + + + + + + + False + 3 + + + + + True + 12 + + + True + 0 + When command _exits: + True + center + exit-action-combobox + + + False + False + 0 + + + + + True + ChildExitedListStore + 0 + + + + 0 + + + + + 1 + + + + + 4 + + + + + False + 0 + + + + + + + False + 1 + + + + + + + 1 + + + + + True + Command + True + center + + + 1 + False + + + + + True + 12 + vertical + 18 + + + True + vertical + 6 + + + True + 0 + <b>Foreground and Background</b> + True + + + False + False + 0 + + + + + True + 12 + + + True + 4 + 2 + 12 + 6 + + + True + 0 + _Text color: + True + center + foreground-colorpicker + + + 2 + 3 + GTK_FILL + + + + + + _Use colors from system theme + True + True + False + True + True + + + + 2 + GTK_FILL + + + + + + True + 0 + _Background color: + True + center + background-colorpicker + + + 3 + 4 + GTK_FILL + + + + + + True + 0 + Built-in sche_mes: + True + center + color-scheme-combobox + + + 1 + 2 + GTK_FILL + + + + + + True + + + True + True + True + Choose Terminal Text Color + #000000000000 + + + False + False + 0 + + + + + + + + 1 + 2 + 2 + 3 + GTK_FILL + GTK_FILL + + + + + True + + + True + True + True + Choose Terminal Background Color + #000000000000 + + + False + False + 0 + + + + + + + + 1 + 2 + 3 + 4 + GTK_FILL + GTK_FILL + + + + + True + ColourSchemeListStore + 2 + + + + + 0 + + + + + 1 + 2 + 1 + 2 + GTK_FILL + + + + + + + False + 1 + + + + + False + 0 + + + + + True + vertical + 6 + + + True + 0 + <b>Palette</b> + True + + + False + False + 0 + + + + + True + 12 + + + True + 3 + 2 + 12 + 6 + + + True + 0 + Built-in _schemes: + True + center + palette-combobox + + + 1 + 2 + GTK_FILL + + + + + + True + 2 + 8 + 6 + 6 + + + True + True + True + #000000000000 + + + + + + + + + True + True + True + #000000000000 + + + 1 + 2 + + + + + + + True + True + True + #000000000000 + + + 2 + 3 + + + + + + + True + True + True + #000000000000 + + + 3 + 4 + + + + + + + True + True + True + #000000000000 + + + 4 + 5 + + + + + + + True + True + True + #000000000000 + + + 5 + 6 + + + + + + + True + True + True + #000000000000 + + + 7 + 8 + + + + + + + True + True + True + #000000000000 + + + 1 + 2 + + + + + + + True + True + True + #000000000000 + + + 1 + 2 + 1 + 2 + + + + + + + True + True + True + #000000000000 + + + 3 + 4 + 1 + 2 + + + + + + + True + True + True + #000000000000 + + + 2 + 3 + 1 + 2 + + + + + + + True + True + True + #000000000000 + + + 4 + 5 + 1 + 2 + + + + + + + True + True + True + #000000000000 + + + 7 + 8 + 1 + 2 + + + + + + + True + True + True + #000000000000 + + + 5 + 6 + 1 + 2 + GTK_FILL + + + + + + True + True + True + #000000000000 + + + 6 + 7 + + + + + + + True + True + True + #000000000000 + + + 6 + 7 + 1 + 2 + GTK_FILL + + + + + + 1 + 2 + 2 + 3 + GTK_FILL + GTK_FILL + + + + + True + 0 + <small><i><b>Note:</b> Terminal applications have these colors available to them.</i></small> + True + center + + + 2 + GTK_FILL + + + + + + True + + + 1 + 2 + 1 + 2 + GTK_FILL + + + + + True + 0 + 0 + Color p_alette: + True + center + palette-colorpicker-1 + + + 2 + 3 + GTK_FILL + GTK_FILL + + + + + + + False + 1 + + + + + False + 1 + + + + + 2 + + + + + True + Colors + True + center + + + 2 + False + + + + + True + 12 + vertical + 6 + + + _Solid color + True + True + False + True + True + True + + + + False + False + 0 + + + + + True + vertical + 6 + + + _Background image + True + True + False + True + True + solid-radiobutton + + + + False + False + 0 + + + + + True + 12 + + + True + vertical + 6 + + + True + 12 + + + True + 0 + Image _file: + True + center + background-image-filechooser + + + False + False + 0 + + + + + True + True + False + Select Background Image + + + 1 + + + + + 0 + + + + + Background image _scrolls + True + True + False + True + True + + + False + False + 1 + + + + + + + False + 1 + + + + + False + 1 + + + + + _Transparent background + True + True + False + True + True + solid-radiobutton + + + + False + False + 2 + + + + + True + vertical + 6 + + + True + 0 + S_hade transparent or image background: + True + darken-background-scale + + + False + False + 1 + 0 + + + + + True + + + True + 0 + 6 + <small><i>None</i></small> + True + + + False + False + 0 + + + + + True + True + delayed + background_darkness_scale + bottom + + + 1 + + + + + True + 1 + 6 + <small><i>Maximum</i></small> + True + + + False + False + 2 + + + + + False + False + 1 + 1 + + + + + False + 3 + + + + + 3 + + + + + True + 0 + Background + True + + + 3 + False + + + + + True + 12 + 4 + 2 + 12 + 6 + + + Scroll on _keystroke + True + True + False + True + True + + + 2 + 3 + 4 + + + + + + Scroll on _output + True + True + False + True + True + + + 2 + 2 + 3 + + + + + + True + + + True + ScrollbarPositionListStore + 0 + + + + 0 + + + + + False + 0 + + + + + 1 + 2 + GTK_FILL + + + + + True + 6 + + + True + True + + ScrollbackAdjustmend + 1 + True + + + False + 0 + + + + + True + 0 + lines + center + scrollback-lines-spinbutton + + + False + False + 1 + + + + + 1 + 2 + 1 + 2 + GTK_FILL + GTK_FILL + + + + + True + 0 + _Scrollbar is: + True + center + scrollbar-position-combobox + + + GTK_FILL + GTK_FILL + + + + + True + 0 + Scroll_back: + True + center + scrollback-lines-spinbutton + + + 1 + 2 + GTK_FILL + GTK_FILL + + + + + 4 + + + + + True + Scrolling + True + + + 4 + False + + + + + True + 12 + vertical + 12 + + + True + 0 + 7.4505801528346183e-09 + <small><i><b>Note:</b> These options may cause some applications to behave incorrectly. They are only here to allow you to work around certain applications and operating systems that expect different terminal behavior.</i></small> + True + True + + + False + 0 + + + + + True + 2 + 3 + 12 + 6 + + + True + 0 + _Delete key generates: + True + center + delete-binding-combobox + + + 1 + 2 + GTK_FILL + + + + + + True + 0 + _Backspace key generates: + True + center + backspace-binding-combobox + + + GTK_FILL + + + + + + True + BackspaceKeyListStore + 1 + + + + 0 + + + + + 1 + 3 + GTK_FILL + GTK_FILL + + + + + True + DeleteKeyListStore + 2 + + + + 0 + + + + + 1 + 3 + 1 + 2 + GTK_FILL + GTK_FILL + + + + + False + 1 + + + + + True + start + + + _Reset Compatibility Options to Defaults + True + True + True + True + + + + False + False + 0 + + + + + False + False + 2 + + + + + 5 + + + + + True + Compatibility + True + center + + + 5 + False + + + + + 1 + + + + + 1 + + + + + True + Profiles + + + 1 + False + + + + + True + Not yet implemented + + + 2 + + + + + True + Layouts + + + 2 + False + + + + + True + True + adjustment4 + never + automatic + + + True + True + KeybindingsListStore + False + 0 + + + Name + + + + 0 + + + + + + + Action + + + + 1 + + + + + + + Keybinding + + + True + + + + + 2 + 3 + + + + + + + + + 3 + + + + + True + Keybindings + + + 3 + False + + + + + True + Not yet implemented + + + 4 + + + + + True + Plugins + + + 4 + False + + + + + 1 + + + + + True + end + + + gtk-cancel + True + True + True + True + + + + False + False + 0 + + + + + gtk-ok + True + True + True + True + + + + False + False + 1 + + + + + False + end + 0 + + + + + + cancelbutton + okbutton + + + + -1 + -1 + 7 + 1 + 2 + 2 + + + 100 + 1 + 10 + 10 + + + 100 + 1 + 10 + 10 + + + 1 + 0.10000000000000001 + 0.10000000000000001 + 1 + + + 10000000 + 1 + 10 + 10 + + + 100 + 1 + 10 + 10 + + diff --git a/terminatorlib/prefs_profile.py b/terminatorlib/prefs_profile.py deleted file mode 100644 index ec6bc911..00000000 --- a/terminatorlib/prefs_profile.py +++ /dev/null @@ -1,442 +0,0 @@ -#!/usr/bin/python - -from terminatorlib.config import dbg,err,DEFAULTS,TerminatorConfValuestoreRC -from terminatorlib.keybindings import TerminatorKeybindings, KeymapError -from terminatorlib.version import APP_NAME, APP_VERSION -from terminatorlib import translation - -import gtk, gobject - -class ProfileEditor: - # lists of which settings to put in which tabs - appearance = ['titlebars', 'zoomedtitlebar', 'titletips', 'allow_bold', 'audible_bell', 'visible_bell', 'urgent_bell', 'force_no_bell', 'background_darkness', 'background_type', 'background_image', 'cursor_blink', 'cursor_shape', 'font', 'scrollbar_position', 'scroll_background', 'use_system_font', 'use_theme_colors', 'enable_real_transparency'] - colours = ['foreground_color','background_color', 'cursor_color', 'palette', 'title_tx_txt_color', 'title_tx_bg_color', 'title_rx_txt_color', 'title_rx_bg_color', 'title_ia_txt_color', 'title_ia_bg_color'] - behaviour = ['backspace_binding', 'delete_binding', 'emulation', 'scroll_on_keystroke', 'scroll_on_output', 'alternate_screen_scroll', 'scrollback_lines', 'focus', 'focus_on_close', 'exit_action', 'word_chars', 'mouse_autohide', 'use_custom_command', 'custom_command', 'http_proxy', 'encoding'] - globals = ['fullscreen', 'maximise', 'borderless', 'handle_size', 'cycle_term_tab', 'close_button_on_tab', 'tab_position', 'copy_on_selection', 'extreme_tabs', 'try_posix_regexp'] - - # metadata about the settings - data = {'titlebars': ['Show titlebars', 'This places a bar above each terminal which displays its title.'], - 'zoomedtitlebar': ['Show titlebar when zoomed', 'This places an informative bar above a zoomed terminal to indicate there are hidden terminals.'], - 'titletips': ['Show title tooltips', 'This adds a tooltip to each terminal which contains its title'], - 'allow_bold': ['Allow bold text', 'Controls whether or not the terminals will honour requests for bold text'], - 'silent_bell': ['', 'When enabled, bell events will generate a flash. When disabled, they will generate a beep'], - 'background_darkness': ['', 'Controls how much the background will be tinted'], - 'scroll_background': ['', 'When enabled the background image will scroll with the text'], - 'force_no_bell': ['', 'Disable both the visual and audible bells'], - 'tab_position': ['', 'Controls the placement of the tab bar'], - 'use_theme_colors': ['', 'Take the foreground and background colours from the current GTK theme'], - 'enable_real_transparency': ['', 'If you are running a composited desktop (e.g. compiz), enabling this option will enable "true" transpraency'], - 'handle_size': ['', 'This controls the size of the border between terminals. Values 0 to 5 are in pixels, while -1 means the value will be decided by your normal GTK theme.'], - 'close_window': ['Quit Terminator', ''], - 'toggle_zoom': ['Toggle maximise terminal', ''], - 'scaled_zoom': ['Toggle zoomed terminal', ''], - 'prev_tab': ['Previous tab', ''], - 'split_vert': ['Split vertically', ''], - 'split_horiz': ['Split horizontally', ''], - 'go_prev': ['Focus previous terminal', ''], - 'go_next': ['Focus next terminal', ''], - 'close_term': ['Close terminal', ''], - 'new_root_tab': ['New root tab', ''], - 'zoom_normal': ['Zoom reset', ''], - 'reset': ['Reset terminal state', ''], - 'reset_clear': ['Reset and clear terminal', ''], - 'hide_window': ['Toggle visibility of the window', ''], - 'title_tx_txt_color': ['Tx Title Foreground Color', ''], - 'title_tx_bg_color': ['Tx Title Background Color', ''], - 'title_rx_txt_color': ['Rx Title Foreground Color', ''], - 'title_rx_bg_color': ['Rx Title Background Color', ''], - 'title_ia_txt_color': ['Inactive Title Foreground Color', ''], - 'title_ia_bg_color': ['Inactive Title Background Color', ''], - } - - # dictionary for results after setting - widgets = {} - - # combobox settings - scrollbar_position = ['left', 'right', 'disabled'] - backspace_del_binding = ['ascii-del', 'control-h', 'escape-sequence', 'delete-sequence'] - focus = ['click', 'sloppy', 'mouse'] - background_type = ['solid', 'image', 'transparent'] - tab_position = ['top', 'bottom', 'left', 'right'] - tab_position_gtk = {'top' : gtk.POS_TOP, 'bottom' : gtk.POS_BOTTOM, 'left' : gtk.POS_LEFT, 'right' : gtk.POS_RIGHT} - cursor_shape = ['block', 'ibeam', 'underline'] - - def __init__ (self, term): - self.term = term - self.window = gtk.Window () - self.notebook = gtk.Notebook() - self.box = gtk.VBox() - self.warning = gtk.Label() - - self.warning.set_use_markup (True) - self.warning.set_line_wrap (True) - self.warning.set_markup ("NOTE: These settings will not be saved. See: man terminator_config") - - self.butbox = gtk.HButtonBox() - self.applybut = gtk.Button(stock=gtk.STOCK_APPLY) - self.applybut.connect ("clicked", self.apply) - self.cancelbut = gtk.Button(stock=gtk.STOCK_CLOSE) - self.cancelbut.connect ("clicked", self.cancel) - - self.box.pack_start(self.warning, False, False) - self.box.pack_start(self.notebook, False, False) - self.box.pack_end(self.butbox, False, False) - - self.butbox.set_layout(gtk.BUTTONBOX_END) - self.butbox.pack_start(self.applybut, False, False) - self.butbox.pack_start(self.cancelbut, False, False) - self.window.add (self.box) - - self.notebook.append_page (self.auto_add (gtk.Table (), self.globals), gtk.Label ("Global Settings")) - self.notebook.append_page (self.prepare_keybindings (), gtk.Label ("Keybindings")) - self.notebook.append_page (self.auto_add (gtk.Table (), self.appearance), gtk.Label ("Appearance")) - self.notebook.append_page (self.auto_add (gtk.Table (), self.colours), gtk.Label ("Colours")) - self.notebook.append_page (self.auto_add (gtk.Table (), self.behaviour), gtk.Label ("Behaviour")) - - def go (self): - self.window.show_all () - - def source_get_type (self, key): - if DEFAULTS.has_key (key): - return DEFAULTS[key].__class__.__name__ - elif DEFAULTS['keybindings'].has_key (key): - return DEFAULTS['keybindings'][key].__class__.__name__ - else: - raise KeyError - - def source_get_value (self, key): - try: - return self.term.conf.__getattr__(key) - except AttributeError: - try: - return self.term.conf.keybindings[key] - except AttributeError: - pass - - def source_get_keyname (self, key): - if self.data.has_key (key) and self.data[key][0] != '': - label_text = self.data[key][0] - else: - label_text = key.replace ('_', ' ').capitalize () - return label_text - - def auto_add (self, table, list): - row = 0 - for key in list: - table.resize (row + 1, 2) - label = gtk.Label (self.source_get_keyname (key)) - wrapperbox = gtk.HBox() - wrapperbox.pack_start(label, False, True) - - type = self.source_get_type (key) - value = self.source_get_value (key) - widget = None - - if key == 'font': - widget = gtk.FontButton(value) - elif key == 'scrollback_lines': - # estimated byte size per line according to g-t: - # sizeof(void *) + sizeof(char *) + sizeof(int) + (80 * (sizeof(int32) + 4) - widget = gtk.SpinButton() - widget.set_digits(0) - widget.set_increments(100, 1000) - widget.set_range(0, 100000) - widget.set_value(value) - elif key == 'scrollbar_position': - if value == 'hidden': - value = 'disabled' - widget = gtk.combo_box_new_text() - for item in self.scrollbar_position: - widget.append_text (item) - if value in self.scrollbar_position: - widget.set_active (self.scrollbar_position.index(value)) - elif key == 'backspace_binding': - widget = gtk.combo_box_new_text() - for item in self.backspace_del_binding: - widget.append_text (item) - if value in self.backspace_del_binding: - widget.set_active (self.backspace_del_binding.index(value)) - elif key == 'delete_binding': - widget = gtk.combo_box_new_text() - for item in self.backspace_del_binding: - widget.append_text (item) - if value in self.backspace_del_binding: - widget.set_active (self.backspace_del_binding.index(value)) - elif key == 'focus': - widget = gtk.combo_box_new_text() - for item in self.focus: - widget.append_text (item) - if value in self.focus: - widget.set_active (self.focus.index(value)) - elif key == 'background_type': - widget = gtk.combo_box_new_text() - for item in self.background_type: - widget.append_text (item) - if value in self.background_type: - widget.set_active (self.background_type.index(value)) - elif key == 'background_darkness': - widget = gtk.HScale () - widget.set_digits (1) - widget.set_draw_value (True) - widget.set_value_pos (gtk.POS_LEFT) - widget.set_range (0, 1) - widget.set_value (value) - elif key == 'handle_size': - widget = gtk.HScale () - widget.set_digits (0) - widget.set_draw_value (True) - widget.set_value_pos (gtk.POS_LEFT) - widget.set_range (-1, 5) - widget.set_value (value) - elif key == 'foreground_color': - widget = gtk.ColorButton (gtk.gdk.color_parse (value)) - elif key == 'background_color': - widget = gtk.ColorButton (gtk.gdk.color_parse (value)) - elif key == 'cursor_color': - if not value: - value = self.source_get_value ('foreground_color') - widget = gtk.ColorButton (gtk.gdk.color_parse (value)) - elif key == 'palette': - colours = value.split (':') - numcolours = len (colours) - widget = gtk.Table (2, numcolours / 2) - x = 0 - y = 0 - for thing in colours: - if x == numcolours / 2: - y += 1 - x = 0 - widget.attach (gtk.ColorButton (gtk.gdk.color_parse (thing)), x, x + 1, y, y + 1) - x += 1 - elif key in ['title_tx_txt_color', 'title_tx_bg_color', 'title_rx_txt_color', 'title_rx_bg_color', 'title_ia_txt_color', 'title_ia_bg_color']: - widget = gtk.ColorButton (gtk.gdk.color_parse (value)) - elif key == 'background_image': - widget = gtk.FileChooserButton('Select a File') - filter = gtk.FileFilter() - filter.add_mime_type ('image/*') - widget.add_filter (filter) - widget.set_local_only (True) - if value: - widget.set_filename (value) - elif key == 'tab_position': - widget = gtk.combo_box_new_text() - for item in self.tab_position: - widget.append_text (item) - if value in self.tab_position: - widget.set_active (self.tab_position.index(value)) - elif key == 'cursor_shape': - widget = gtk.combo_box_new_text() - for item in self.cursor_shape: - widget.append_text (item) - if value in self.cursor_shape: - widget.set_active (self.cursor_shape.index (value)) - else: - if type == "bool": - widget = gtk.CheckButton () - widget.set_active (value) - elif type in ["str", "int", "float"]: - widget = gtk.Entry () - widget.set_text (str(value)) - elif type == "list": - continue - else: - err("Unknown type: %s for key: %s" % (type, key)) - continue - - if hasattr(widget, 'set_tooltip_text') and self.data.has_key (key): - widget.set_tooltip_text (self.data[key][1]) - - widget.set_name(key) - self.widgets[key] = widget - table.attach (wrapperbox, 0, 1, row, row + 1, gtk.EXPAND|gtk.FILL, gtk.FILL) - table.attach (widget, 1, 2, row, row + 1, gtk.EXPAND|gtk.FILL, gtk.FILL) - row += 1 - - return (table) - - def apply (self, data): - values = {} - for page in [self.appearance, self.behaviour, self.globals, self.colours]: - for property in page: - widget = self.widgets[property] - - if isinstance (widget, gtk.SpinButton): - value = widget.get_value () - elif isinstance (widget, gtk.Entry): - value = widget.get_text() - elif isinstance (widget, gtk.CheckButton): - value = widget.get_active() - elif isinstance (widget, gtk.ComboBox): - if widget.name == 'scrollbar_position': - bucket = self.scrollbar_position - elif widget.name == 'backspace_binding' or widget.name == 'delete_binding': - bucket = self.backspace_del_binding - elif widget.name == 'focus': - bucket = self.focus - elif widget.name == 'background_type': - bucket = self.background_type - elif widget.name == 'tab_position': - bucket = self.tab_position - elif widget.name == 'cursor_shape': - bucket = self.cursor_shape - else: - err("Unknown bucket type for %s" % widget.name) - continue - - value = bucket[widget.get_active()] - elif isinstance (widget, gtk.FontButton): - value = widget.get_font_name() - elif isinstance (widget, gtk.HScale): - value = widget.get_value() - if widget.get_digits() == 0: - value = int(value) - elif isinstance (widget, gtk.ColorButton): - value = widget.get_color().to_string() - elif isinstance (widget, gtk.FileChooserButton): - value = widget.get_filename() - elif widget.get_name() == 'palette': - value = '' - valuebits = [] - children = widget.get_children() - children.reverse() - for child in children: - valuebits.append (child.get_color().to_string()) - value = ':'.join (valuebits) - else: - value = None - err("skipping unknown property: %s" % property) - - values[property] = value - - has_changed = False - changed = [] - for source in self.term.conf.sources: - if isinstance (source, TerminatorConfValuestoreRC): - for property in values: - try: - if self.source_get_value(property) != values[property]: - dbg("%s changed from %s to %s" % (property, self.source_get_value(property), values[property])) - source.values[property] = values[property] - has_changed = True - changed.append(property) - except KeyError: - pass - if has_changed: - for changer in changed: - if changer == "fullscreen": - self.term.fullscreen_absolute(values[changer]) - elif changer == "maximise": - if values[changer]: - self.term.maximize() - else: - self.term.unmaximize() - elif changer == "borderless": - self.term.window.set_decorated (not values[changer]) - elif changer == "handle_size": - self.term.set_handle_size(values[changer]) - gtk.rc_reset_styles(gtk.settings_get_default()) - elif changer == "tab_position": - notebook = self.term.window.get_child() - new_pos = self.tab_position_gtk[values[changer]] - angle = 0 - if isinstance (notebook, gtk.Notebook): - notebook.set_tab_pos(new_pos) - for i in xrange(0,notebook.get_n_pages()): - notebook.get_tab_label(notebook.get_nth_page(i)).update_angle() - pass - elif changer == "close_button_on_tab": - notebook = self.term.window.get_child() - if isinstance (notebook, gtk.Notebook): - for i in xrange(0,notebook.get_n_pages()): - notebook.get_tab_label(notebook.get_nth_page(i)).update_closebut() - # FIXME: which others? cycle_term_tab, copy_on_selection, extreme_tabs, try_posix_regexp - - self.term.reconfigure_vtes() - - # Check for changed keybindings - changed_keybindings = [] - for row in self.liststore: - accel = gtk.accelerator_name (row[2], row[3]) - value = self.term.conf.keybindings[row[0]] - if isinstance (value, tuple): - value = value[0] - keyval = 0 - mask = 0 - if value is not None and value != "None": - try: - (keyval, mask) = self.tkbobj._parsebinding(value) - except KeymapError: - pass - if (row[2], row[3]) != (keyval, mask): - changed_keybindings.append ((row[0], accel)) - dbg("%s changed from %s to %s" % (row[0], self.term.conf.keybindings[row[0]], accel)) - - newbindings = self.term.conf.keybindings - for binding in changed_keybindings: - newbindings[binding[0]] = binding[1] - self.term.keybindings.configure (newbindings) - - def cancel (self, data): - self.window.destroy() - self.term.options = None - del(self) - - def prepare_keybindings (self): - self.liststore = gtk.ListStore (gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_UINT, gobject.TYPE_UINT, gobject.TYPE_BOOLEAN) - self.liststore.set_sort_column_id (0, gtk.SORT_ASCENDING) - self.tkbobj = TerminatorKeybindings() - keyval = None - mask = None - - for binding in DEFAULTS['keybindings']: - value = self.term.conf.keybindings[binding] - keyval = 0 - mask = 0 - if isinstance (value, tuple): - value = value[0] - if value is not None and value != "None": - try: - (keyval, mask) = self.tkbobj._parsebinding (value) - except KeymapError: - pass - self.liststore.append ([binding, self.source_get_keyname (binding), keyval, mask, True]) - dbg("Appended row: %s, %s, %s" % (binding, keyval, mask)) - - self.treeview = gtk.TreeView(self.liststore) - - cell = gtk.CellRendererText() - col = gtk.TreeViewColumn(_("Name")) - col.pack_start(cell, True) - col.add_attribute(cell, "text", 0) - - self.treeview.append_column(col) - - cell = gtk.CellRendererText() - col = gtk.TreeViewColumn(_("Action")) - col.pack_start(cell, True) - col.add_attribute(cell, "text", 1) - - self.treeview.append_column(col) - - cell = gtk.CellRendererAccel() - col = gtk.TreeViewColumn(_("Keyboard shortcut")) - col.pack_start(cell, True) - col.set_attributes(cell, accel_key=2, accel_mods=3, editable=4) - - cell.connect ('accel-edited', self.edited) - cell.connect ('accel-cleared', self.cleared) - - self.treeview.append_column(col) - - scrollwin = gtk.ScrolledWindow () - scrollwin.set_policy (gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) - scrollwin.add (self.treeview) - return (scrollwin) - - def edited (self, obj, path, key, mods, code): - iter = self.liststore.get_iter_from_string(path) - self.liststore.set(iter, 2, key, 3, mods) - - def cleared (self, obj, path): - iter = self.liststore.get_iter_from_string(path) - self.liststore.set(iter, 2, 0, 3, 0) diff --git a/terminatorlib/prefseditor.py b/terminatorlib/prefseditor.py new file mode 100755 index 00000000..035743cc --- /dev/null +++ b/terminatorlib/prefseditor.py @@ -0,0 +1,863 @@ +#!/usr/bin/python + +import os +import gtk +import gobject + +from util import dbg +import config +from keybindings import Keybindings, KeymapError +from translation import _ +from terminator import Terminator + +# FIXME: We need to check that we have represented all of Config() below +class PrefsEditor: + config = None + keybindings = None + window = None + builder = None + previous_selection = None + colorschemevalues = {'black_on_yellow': 0, + 'black_on_white': 1, + 'grey_on_black': 2, + 'green_on_black': 3, + 'white_on_black': 4, + 'orange_on_black': 5, + 'custom': 6} + + keybindingnames = { 'zoom_in' : 'Increase font size', + 'zoom_out' : 'Decrease font size', + 'zoom_normal' : 'Restore original font size', + 'new_tab' : 'Create a new tab', + 'cycle_next' : 'Focus the next terminal', + 'cycle_prev' : 'Focus the previous terminal', + 'go_next' : 'Focus the next terminal', + 'go_prev' : 'Focus the previous terminal', + 'go_up' : 'Focus the terminal above', + 'go_down' : 'Focus the terminal below', + 'go_left' : 'Focus the terminal left', + 'go_right' : 'Focus the terminal right', + 'split_horiz' : 'Split horizontally', + 'split_vert' : 'Split vertically', + 'close_term' : 'Close terminal', + 'copy' : 'Copy selected text', + 'paste' : 'Paste clipboard', + 'toggle_scrollbar' : 'Show/Hide the scrollbar', + 'search' : 'Search terminal scrollback', + 'close_window' : 'Close window', + 'resize_up' : 'Resize the terminal up', + 'resize_down' : 'Resize the terminal down', + 'resize_left' : 'Resize the terminal left', + 'resize_right' : 'Resize the terminal right', + 'move_tab_right' : 'Move the tab right', + 'move_tab_left' : 'Move the tab left', + 'toggle_zoom' : 'Maximise terminal', + 'scaled_zoom' : 'Zoom terminal', + 'next_tab' : 'Switch to the next tab', + 'prev_tab' : 'Switch to the previous tab', + 'switch_to_tab_1' : 'Switch to the first tab', + 'switch_to_tab_2' : 'Switch to the second tab', + 'switch_to_tab_3' : 'Switch to the third tab', + 'switch_to_tab_4' : 'Switch to the fourth tab', + 'switch_to_tab_5' : 'Switch to the fifth tab', + 'switch_to_tab_6' : 'Switch to the sixth tab', + 'switch_to_tab_7' : 'Switch to the seventh tab', + 'switch_to_tab_8' : 'Switch to the eighth tab', + 'switch_to_tab_9' : 'Switch to the ninth tab', + 'switch_to_tab_10' : 'Switch to the tenth tab', + 'full_screen' : 'Toggle fullscreen', + 'reset' : 'Reset the terminal', + 'reset_clear' : 'Reset and clear the terminal', + 'hide_window' : 'Toggle window visibility', + 'group_all' : 'Group all terminals', + 'ungroup_all' : 'Ungroup all terminals', + 'group_tab' : 'Group terminals in tab', + 'ungroup_tab' : 'Ungroup terminals in tab', + 'new_window' : 'Create a new window', + } + + def __init__ (self, term): + self.config = config.Config() + self.term = term + self.builder = gtk.Builder() + self.keybindings = Keybindings() + try: + # Figure out where our library is on-disk so we can open our + (head, tail) = os.path.split(config.__file__) + librarypath = os.path.join(head, 'preferences.glade') + gladefile = open(librarypath, 'r') + gladedata = gladefile.read() + except Exception, ex: + print "Failed to find preferences.glade" + print ex + return + + self.builder.add_from_string(gladedata) + self.window = self.builder.get_object('prefswin') + self.set_values() + self.builder.connect_signals(self) + self.window.show_all() + + def on_cancelbutton_clicked(self, button): + """Close the window""" + self.window.destroy() + del(self) + + def on_okbutton_clicked(self, button): + """Save the config""" + self.store_values() + self.config.save() + terminator = Terminator() + terminator.reconfigure() + self.window.destroy() + del(self) + + def set_values(self): + """Update the preferences window with all the configuration from + Config()""" + guiget = self.builder.get_object + + ## Global tab + # Mouse focus + focus = self.config['focus'] + active = 0 + if focus == 'click': + active = 1 + elif focus in ['sloppy', 'mouse']: + active = 2 + widget = guiget('focuscombo') + widget.set_active(active) + # Terminal separator size + termsepsize = self.config['handle_size'] + widget = guiget('handlesize') + widget.set_value(float(termsepsize)) + # Window geometry hints + geomhint = self.config['geometry_hinting'] + widget = guiget('wingeomcheck') + widget.set_active(geomhint) + # Window state + option = self.config['window_state'] + if option == 'hidden': + active = 1 + elif option == 'maximise': + active = 2 + elif option == 'fullscreen': + active = 3 + else: + active = 0 + widget = guiget('winstatecombo') + widget.set_active(active) + # Window borders + widget = guiget('winbordercheck') + widget.set_active(not self.config['borderless']) + # Tab bar position + option = self.config['tab_position'] + widget = guiget('tabposcombo') + if option == 'bottom': + active = 1 + elif option == 'left': + active = 2 + elif option == 'right': + active = 3 + else: + active = 0 + widget.set_active(active) + + ## Profile tab + # Populate the profile list + widget = guiget('profilelist') + liststore = widget.get_model() + profiles = self.config.list_profiles() + self.profileiters = {} + for profile in profiles: + self.profileiters[profile] = liststore.append([profile]) + selection = widget.get_selection() + selection.connect('changed', self.on_profile_selection_changed) + selection.select_iter(self.profileiters['default']) + + ## Layouts tab + # FIXME: Implement this + + ## Keybindings tab + widget = guiget('keybindingtreeview') + liststore = widget.get_model() + liststore.set_sort_column_id(0, gtk.SORT_ASCENDING) + keybindings = self.config['keybindings'] + for keybinding in keybindings: + keyval = 0 + mask = 0 + value = keybindings[keybinding] + if value is not None and value != '': + try: + (keyval, mask) = self.keybindings._parsebinding(value) + except KeymapError: + pass + liststore.append([keybinding, self.keybindingnames[keybinding], + keyval, mask]) + + ## Plugins tab + # FIXME: Implement this + + def store_values(self): + """Store the values from the GUI back into Config()""" + guiget = self.builder.get_object + + ## Global tab + # Focus + widget = guiget('focuscombo') + selected = widget.get_active() + if selected == 0: + value = 'system' + elif selected == 1: + value = 'click' + elif selected == 2: + value = 'mouse' + self.config['focus'] = value + # Handle size + widget = guiget('handlesize') + self.config['handle_size'] = int(widget.get_value()) + # Window geometry + widget = guiget('wingeomcheck') + self.config['geometry_hinting'] = widget.get_active() + # Window state + widget = guiget('winstatecombo') + selected = widget.get_active() + if selected == 0: + value = 'normal' + elif selected == 1: + value = 'hidden' + elif selected == 2: + value = 'maximise' + elif selected == 3: + value = 'fullscreen' + self.config['window_state'] = value + # Window borders + widget = guiget('winbordercheck') + self.config['borderless'] = not widget.get_active() + # Tab position + widget = guiget('tabposcombo') + selected = widget.get_active() + if selected == 0: + value = 'top' + elif selected == 1: + value = 'bottom' + elif selected == 2: + value = 'left' + elif selected == 3: + value = 'right' + self.config['tab_position'] = value + + ## Profile tab + self.store_profile_values(self.previous_selection) + + ## Layouts tab + # FIXME: Implement this + + ## Keybindings tab + keybindings = self.config['keybindings'] + liststore = guiget('KeybindingsListStore') + for keybinding in liststore: + accel = gtk.accelerator_name(keybinding[2], keybinding[3]) + keybindings[keybinding[0]] = accel + + ## Plugins tab + # FIXME: Implement this + + def set_profile_values(self, profile): + """Update the profile values for a given profile""" + self.config.set_profile(profile) + guiget = self.builder.get_object + + dbg('PrefsEditor::set_profile_values: Setting profile %s' % profile) + + ## General tab + # Use system font + widget = guiget('system-font-checkbutton') + widget.set_active(self.config['use_system_font']) + self.on_system_font_checkbutton_toggled(widget) + # Font selector + widget = guiget('font-selector') + widget.set_font_name(self.config['font']) + # Allow bold text + widget = guiget('allow-bold-checkbutton') + widget.set_active(self.config['allow_bold']) + # Visual terminal bell + widget = guiget('visual-bell-checkbutton') + widget.set_active(self.config['visible_bell']) + # Audible terminal bell + widget = guiget('audible-bell-checkbutton') + widget.set_active(self.config['audible_bell']) + # WM_URGENT terminal bell + widget = guiget('urgent-bell-checkbutton') + widget.set_active(self.config['urgent_bell']) + # Cursor shape + widget = guiget('cursor-shape-combobox') + if self.config['cursor_shape'] == 'underline': + active = 1 + elif self.config['cursor_shape'] == 'ibeam': + active = 2 + else: + active = 0 + widget.set_active(active) + # Word chars + widget = guiget('word-chars-entry') + widget.set_text(self.config['word_chars']) + + ## Command tab + # Login shell + widget = guiget('login-shell-checkbutton') + widget.set_active(self.config['login_shell']) + # Login records + widget = guiget('update-records-checkbutton') + widget.set_active(self.config['update_records']) + # Use Custom command + widget = guiget('use-custom-command-checkbutton') + widget.set_active(self.config['use_custom_command']) + self.on_use_custom_command_checkbutton_toggled(widget) + # Custom Command + widget = guiget('custom-command-entry') + widget.set_text(self.config['custom_command']) + # Exit action + widget = guiget('exit-action-combobox') + if self.config['exit_action'] == 'restart': + widget.set_active(1) + elif self.config['exit_action'] == 'hold': + widget.set_active(2) + else: + # Default is to close the terminal + widget.set_active(0) + + ## Colors tab + # Use system colors + widget = guiget('use-theme-colors-checkbutton') + widget.set_active(self.config['use_theme_colors']) + # Colorscheme + widget = guiget('color-scheme-combobox') + scheme = self.config['color_scheme'] + if scheme not in self.colorschemevalues: + scheme = 'grey_on_black' + widget.set_active(self.colorschemevalues[scheme]) + # Foreground color + widget = guiget('foreground-colorpicker') + widget.set_color(gtk.gdk.Color(self.config['foreground_color'])) + if scheme == 'custom': + widget.set_sensitive(True) + else: + widget.set_sensitive(False) + # Background color + widget = guiget('background-colorpicker') + widget.set_color(gtk.gdk.Color(self.config['background_color'])) + if scheme == 'custom': + widget.set_sensitive(True) + else: + widget.set_sensitive(False) + # Palette + palette = self.config['palette'].split(':') + for i in xrange(1,17): + widget = guiget('palette-colorpicker-%d' % i) + widget.set_color(gtk.gdk.Color(palette[i - 1])) + + ## Background tab + # Radio values + self.update_background_tab() + # Background image file + if self.config['background_image'] != '': + widget = guiget('background-image-filechooser') + if self.config['background_image'] is not None and \ + self.config['background_image'] != '': + widget.set_filename(self.config['background_image']) + # Background image scrolls + widget = guiget('scroll-background-checkbutton') + widget.set_active(self.config['scroll_background']) + # Background shading + widget = guiget('background_darkness_scale') + widget.set_value(float(self.config['background_darkness'])) + + if self.config['background_type'] == 'solid': + guiget('solid-radiobutton').set_active(True) + elif self.config['background_type'] == 'image': + guiget('image-radiobutton').set_active(True) + elif self.config['background_type'] == 'transparent': + guiget('trans-radiobutton').set_active(True) + + ## Scrolling tab + # Scrollbar position + widget = guiget('scrollbar-position-combobox') + value = self.config['scrollbar_position'] + if value == 'left': + widget.set_active(0) + elif value in ['disabled', 'hidden']: + widget.set_active(2) + else: + widget.set_active(1) + # Scrollback lines + widget = guiget('scrollback-lines-spinbutton') + widget.set_value(self.config['scrollback_lines']) + # Scroll on outut + widget = guiget('scroll-on-output-checkbutton') + widget.set_active(self.config['scroll_on_output']) + # Scroll on keystroke + widget = guiget('scroll-on-keystroke-checkbutton') + widget.set_active(self.config['scroll_on_keystroke']) + + ## Compatibility tab + # Backspace key + widget = guiget('backspace-binding-combobox') + value = self.config['backspace_binding'] + if value == 'control-h': + widget.set_active(0) + elif value == 'escape-sequence': + widget.set_active(2) + else: + widget.set_active(1) + # Delete key + widget = guiget('delete-binding-combobox') + value = self.config['delete_binding'] + if value == 'control-h': + widget.set_active(0) + elif value == 'ascii-del': + widget.set_active(1) + else: + widget.set_active(2) + + def store_profile_values(self, profile): + """Pull out all the settings before switching profile""" + guiget = self.builder.get_object + + ## General tab + # Use system font + widget = guiget('system-font-checkbutton') + self.config['use_system_font'] = widget.get_active() + # Font + widget = guiget('font-selector') + self.config['font'] = widget.get_font_name() + # Allow bold + widget = guiget('allow-bold-checkbutton') + self.config['allow_bold'] = widget.get_active() + # Visual Bell + widget = guiget('visual-bell-checkbutton') + self.config['visible_bell'] = widget.get_active() + # Audible Bell + widget = guiget('audible-bell-checkbutton') + self.config['audible_bell'] = widget.get_active() + # Urgent Bell + widget = guiget('urgent-bell-checkbutton') + self.config['urgent_bell'] = widget.get_active() + # Cursor Shape + widget = guiget('cursor-shape-combobox') + selected = widget.get_active() + if selected == 0: + value = 'block' + elif selected == 1: + value = 'underline' + elif selected == 2: + value = 'ibeam' + self.config['cursor_shape'] = value + # Word chars + widget = guiget('word-chars-entry') + self.config['word_chars'] = widget.get_text() + + ## Command tab + # Login shell + widget = guiget('login-shell-checkbutton') + self.config['login_shell'] = widget.get_active() + # Update records + widget = guiget('update-records-checkbutton') + self.config['update_records'] = widget.get_active() + # Use custom command + widget = guiget('use-custom-command-checkbutton') + self.config['use_custom_command'] = widget.get_active() + # Custom command + widget = guiget('custom-command-entry') + self.config['custom_command'] = widget.get_text() + # Exit action + widget = guiget('exit-action-combobox') + selected = widget.get_active() + if selected == 0: + value = 'close' + elif selected == 1: + value = 'restart' + elif selected == 2: + value = 'hold' + self.config['exit_action'] = value + + ## Colours tab + # Use system colours + widget = guiget('use-theme-colors-checkbutton') + self.config['use_theme_colors'] = widget.get_active() + # Colour scheme + widget = guiget('color-scheme-combobox') + selected = widget.get_active() + if selected == 0: + value = 'black_on_yellow' + elif selected == 1: + value = 'black_on_white' + elif selected == 2: + value = 'grey_on_black' + elif selected == 3: + value = 'green_on_black' + elif selected == 4: + value = 'white_on_black' + elif selected == 5: + value = 'orange_on_black' + elif selected == 6: + value = 'custom' + self.config['color_scheme'] = value + # Foreground colour + widget = guiget('foreground-colorpicker') + self.config['foreground_color'] = widget.get_color().to_string() + # Background colour + widget = guiget('background-colorpicker') + self.config['background_color'] = widget.get_color().to_string() + # Palette + palette = [] + for i in xrange(1,17): + widget = guiget('palette-colorpicker-%d' % i) + palette.append(widget.get_color().to_string()) + self.config['palette'] = ':'.join(palette) + + ## Background tab + # Background type + widget = guiget('solid-radiobutton') + if widget.get_active() == True: + value = 'solid' + widget = guiget('image-radiobutton') + if widget.get_active() == True: + value = 'image' + widget = guiget('transparent-radiobutton') + if widget.get_active() == True: + value = 'transparent' + # Background image + widget = guiget('background-image-filechooser') + self.config['background_image'] = widget.get_filename() + # Background scrolls + widget = guiget('scroll-background-checkbutton') + self.config['scroll_background'] = widget.get_active() + # Background darkness + widget = guiget('darken-background-scale') + self.config['background_darkness'] = widget.get_value() + + ## Scrolling tab + # Scrollbar + widget = guiget('scrollbar-position-combobox') + selected = widget.get_active() + if selected == 0: + value = 'left' + elif selected == 1: + value = 'right' + elif selected == 2: + value = 'hidden' + self.config['scrollbar_position'] = value + # Scrollback lines + widget = guiget('scrollback-lines-spinbutton') + self.config['scrollback_lines'] = int(widget.get_value()) + # Scroll on output + widget = guiget('scroll-on-output-checkbutton') + self.config['scroll_on_output'] = widget.get_active() + # Scroll on keystroke + widget = guiget('scroll-on-keystroke-checkbutton') + self.config['scroll_on_keystroke'] = widget.get_active() + + ## Compatibility tab + # Backspace key + widget = guiget('backspace-binding-combobox') + selected = widget.get_active() + if selected == 0: + value = 'control-h' + elif selected == 1: + value = 'ascii-del' + elif selected == 2: + value =='escape-sequence' + self.config['backspace_binding'] = value + # Delete key + widget = guiget('delete-binding-combobox') + selected = widget.get_active() + if selected == 0: + value = 'control-h' + elif selected == 1: + value = 'ascii-del' + elif selected == 2: + value = 'escape-sequence' + self.config['delete_binding'] = value + + def on_profileaddbutton_clicked(self, button): + """Add a new profile to the list""" + guiget = self.builder.get_object + + treeview = guiget('profilelist') + model = treeview.get_model() + values = [ r[0] for r in model ] + + newprofile = _('New Profile') + if newprofile in values: + i = 1 + while newprofile in values: + i = i + 1 + newprofile = '%s %d' % (_('New Profile'), i) + + if self.config.add_profile(newprofile): + model.append([newprofile]) + + def on_profileremovebutton_clicked(self, button): + """Remove a profile from the list""" + guiget = self.builder.get_object + + treeview = guiget('profilelist') + selection = treeview.get_selection() + (model, rowiter) = selection.get_selected() + profile = model.get_value(rowiter, 0) + + if profile == 'default': + # We shouldn't let people delete this profile + return + + self.previous_selection = None + self.config.del_profile(profile) + model.remove(rowiter) + selection.select_iter(model.get_iter_first()) + + def on_use_custom_command_checkbutton_toggled(self, checkbox): + """Toggling the use_custom_command checkbox needs to alter the + sensitivity of the custom_command entrybox""" + guiget = self.builder.get_object + + widget = guiget('custom-command-entry') + if checkbox.get_active() == True: + widget.set_sensitive(True) + else: + widget.set_sensitive(False) + + def on_system_font_checkbutton_toggled(self, checkbox): + """Toggling the use_system_font checkbox needs to alter the + sensitivity of the font selector""" + guiget = self.builder.get_object + + widget = guiget('font-selector') + if checkbox.get_active() == True: + widget.set_sensitive(False) + else: + widget.set_sensitive(True) + + def on_reset_compatibility_clicked(self, widget): + """Reset the confusing and annoying backspace/delete options to the + safest values""" + guiget = self.builder.get_object + + widget = guiget('backspace-binding-combobox') + widget.set_active(1) + widget = guiget('delete-binding-combobox') + widget.set_active(2) + + def on_background_type_toggled(self, widget): + """The background type was toggled""" + self.update_background_tab() + + def update_background_tab(self): + """Update the background tab""" + guiget = self.builder.get_object + + # Background type + backtype = None + solidwidget = guiget('solid-radiobutton') + imagewidget = guiget('image-radiobutton') + transwidget = guiget('transparent-radiobutton') + if transwidget.get_active() == True: + backtype = 'trans' + elif imagewidget.get_active() == True: + backtype = 'image' + else: + backtype = 'solid' + if backtype == 'image': + guiget('background-image-filechooser').set_sensitive(True) + guiget('scroll-background-checkbutton').set_sensitive(True) + else: + guiget('background-image-filechooser').set_sensitive(False) + guiget('scroll-background-checkbutton').set_sensitive(False) + if backtype == 'trans': + guiget('darken-background-scale').set_sensitive(True) + else: + guiget('darken-background-scale').set_sensitive(False) + + def on_profile_selection_changed(self, selection): + """A different profile was selected""" + if self.previous_selection is not None: + dbg('PrefsEditor::on_profile_selection_changed: Storing: %s' % + self.previous_selection) + self.store_profile_values(self.previous_selection) + + (listmodel, rowiter) = selection.get_selected() + if not rowiter: + # Something is wrong, just jump to the first item in the list + treeview = selection.get_tree_view() + liststore = treeview.get_model() + selection.select_iter(liststore.get_iter_first()) + return + profile = listmodel.get_value(rowiter, 0) + self.set_profile_values(profile) + self.previous_selection = profile + + widget = self.builder.get_object('profileremovebutton') + if profile == 'default': + widget.set_sensitive(False) + else: + widget.set_sensitive(True) + + def on_profile_name_edited(self, cell, path, newtext): + """Update a profile name""" + oldname = cell.get_property('text') + if oldname == newtext: + return + dbg('PrefsEditor::on_profile_name_edited: Changing %s to %s' % + (oldname, newtext)) + self.config.rename_profile(oldname, newtext) + + widget = self.builder.get_object('profilelist') + model = widget.get_model() + iter = model.get_iter(path) + model.set_value(iter, 0, newtext) + + if oldname == self.previous_selection: + self.previous_selection = newtext + + def on_color_scheme_combobox_changed(self, widget): + """Update the fore/background colour pickers""" + value = None + guiget = self.builder.get_object + active = widget.get_active() + for key in self.colorschemevalues.keys(): + if self.colorschemevalues[key] == active: + value = key + + fore = guiget('foreground-colorpicker') + back = guiget('background-colorpicker') + if value == 'custom': + fore.set_sensitive(True) + back.set_sensitive(True) + else: + fore.set_sensitive(False) + back.set_sensitive(False) + + forecol = None + backcol = None + if value == 'grey_on_black': + forecol = '#AAAAAA' + backcol = '#000000' + elif value == 'black_on_yellow': + forecol = '#000000' + backcol = '#FFFFDD' + elif value == 'black_on_white': + forecol = '#000000' + backcol = '#FFFFFF' + elif value == 'white_on_black': + forecol = '#FFFFFF' + backcol = '#000000' + elif value == 'green_on_black': + forecol = '#00FF00' + backcol = '#000000' + elif value == 'orange_on_black': + forecol = '#E53C00' + backcol = '#000000' + + if forecol is not None: + fore.set_color(gtk.gdk.Color(forecol)) + if backcol is not None: + back.set_color(gtk.gdk.Color(backcol)) + + def on_use_theme_colors_checkbutton_toggled(self, widget): + """Update colour pickers""" + guiget = self.builder.get_object + active = widget.get_active() + + scheme = guiget('color-scheme-combobox') + fore = guiget('foreground-colorpicker') + back = guiget('background-colorpicker') + + if active: + for widget in [scheme, fore, back]: + widget.set_sensitive(False) + else: + scheme.set_sensitive(True) + self.on_color_scheme_combobox_changed(scheme) + + def on_cellrenderer_accel_edited(self, liststore, path, key, mods, code): + """Handle an edited keybinding""" + celliter = liststore.get_iter_from_string(path) + liststore.set(celliter, 2, key, 3, mods) + + def on_cellrenderer_accel_cleared(self, liststore, path): + celliter = liststore.get_iter_from_string(path) + liststore.set(celliter, 2, 0, 3, 0) + + def source_get_keyname (self, key): + if self.data.has_key (key) and self.data[key][0] != '': + label_text = self.data[key][0] + else: + label_text = key.replace ('_', ' ').capitalize () + return label_text + + def prepare_keybindings (self): + self.liststore = gtk.ListStore (gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_UINT, gobject.TYPE_UINT, gobject.TYPE_BOOLEAN) + self.liststore.set_sort_column_id (0, gtk.SORT_ASCENDING) + self.tkbobj = Keybindings() + keyval = None + mask = None + + for binding in config.DEFAULTS['keybindings']: + value = self.config['keybindings'][binding] + keyval = 0 + mask = 0 + if isinstance (value, tuple): + value = value[0] + if value is not None and value != "None": + try: + (keyval, mask) = self.tkbobj._parsebinding (value) + except KeymapError: + pass + self.liststore.append ([binding, self.source_get_keyname (binding), keyval, mask, True]) + dbg("Appended row: %s, %s, %s" % (binding, keyval, mask)) + + self.treeview = gtk.TreeView(self.liststore) + + cell = gtk.CellRendererText() + col = gtk.TreeViewColumn(_("Name")) + col.pack_start(cell, True) + col.add_attribute(cell, "text", 0) + + self.treeview.append_column(col) + + cell = gtk.CellRendererText() + col = gtk.TreeViewColumn(_("Action")) + col.pack_start(cell, True) + col.add_attribute(cell, "text", 1) + + self.treeview.append_column(col) + + cell = gtk.CellRendererAccel() + col = gtk.TreeViewColumn(_("Keyboard shortcut")) + col.pack_start(cell, True) + col.set_attributes(cell, accel_key=2, accel_mods=3, editable=4) + + cell.connect ('accel-edited', self.edited) + cell.connect ('accel-cleared', self.cleared) + + self.treeview.append_column(col) + + scrollwin = gtk.ScrolledWindow () + scrollwin.set_policy (gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + scrollwin.add (self.treeview) + return (scrollwin) + + def edited (self, obj, path, key, mods, code): + iter = self.liststore.get_iter_from_string(path) + self.liststore.set(iter, 2, key, 3, mods) + + def cleared (self, obj, path): + iter = self.liststore.get_iter_from_string(path) + self.liststore.set(iter, 2, 0, 3, 0) + +if __name__ == '__main__': + import terminal + term = terminal.Terminal() + foo = PrefsEditor(term) + + gtk.main() diff --git a/terminatorlib/pylint.sh b/terminatorlib/pylint.sh new file mode 100755 index 00000000..208bdb7a --- /dev/null +++ b/terminatorlib/pylint.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +for file in *.py; do + line=$(pylint $file 2>&1 | grep "^Your code has been rated") + rating=$(echo $line | cut -f 7 -d ' ') + previous=$(echo $line | cut -f 10 -d ' ') + + if [ "$rating" != "10.00/10" ]; then + echo "$file rated $rating (previously $previous)" + fi +done diff --git a/terminatorlib/searchbar.py b/terminatorlib/searchbar.py new file mode 100755 index 00000000..46b65900 --- /dev/null +++ b/terminatorlib/searchbar.py @@ -0,0 +1,194 @@ +#!/usr/bin/python +# Terminator by Chris Jones +# GPL v2 only +"""searchbar.py - classes necessary to provide a terminal search bar""" + +import gtk +import gobject + +from translation import _ +from config import Config + +# pylint: disable-msg=R0904 +class Searchbar(gtk.HBox): + """Class implementing the Searchbar widget""" + + __gsignals__ = { + 'end-search': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + } + + entry = None + reslabel = None + next = None + prev = None + + vte = None + config = None + + searchstring = None + searchrow = None + + searchits = None + + def __init__(self): + """Class initialiser""" + gtk.HBox.__init__(self) + self.__gobject_init__() + + self.config = Config() + + # Search text + self.entry = gtk.Entry() + self.entry.set_activates_default(True) + self.entry.show() + self.entry.connect('activate', self.do_search) + self.entry.connect('key-press-event', self.search_keypress) + + # Label + label = gtk.Label(_('Search:')) + label.show() + + # Result label + self.reslabel = gtk.Label('') + self.reslabel.show() + + # Close Button + close = gtk.Button() + close.set_relief(gtk.RELIEF_NONE) + close.set_focus_on_click(False) + icon = gtk.Image() + icon.set_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU) + close.add(icon) + close.set_name('terminator-search-close-button') + if hasattr(close, 'set_tooltip_text'): + close.set_tooltip_text(_('Close Search bar')) + close.connect('clicked', self.end_search) + close.show_all() + + # Next Button + self.next = gtk.Button(_('Next')) + self.next.show() + self.next.set_sensitive(False) + self.next.connect('clicked', self.next_search) + + # Previous Button + self.prev = gtk.Button(_('Prev')) + self.prev.show() + self.prev.set_sensitive(False) + self.prev.connect('clicked', self.prev_search) + + self.pack_start(label, False) + self.pack_start(self.entry) + self.pack_start(self.reslabel, False) + self.pack_start(self.prev, False, False) + self.pack_start(self.next, False, False) + self.pack_end(close, False, False) + + self.hide() + self.set_no_show_all(True) + + def get_vte(self): + """Find our parent widget""" + parent = self.get_parent() + if parent: + self.vte = parent.vte + + # pylint: disable-msg=W0613 + def search_keypress(self, widget, event): + """Handle keypress events""" + key = gtk.gdk.keyval_name(event.keyval) + if key == 'Escape': + self.end_search() + + def start_search(self): + """Show ourselves""" + if not self.vte: + self.get_vte() + + self.show() + self.entry.grab_focus() + + def do_search(self, widget): + """Trap and re-emit the clicked signal""" + searchtext = self.entry.get_text() + if searchtext == '': + return + + if searchtext != self.searchstring: + self.searchrow = self.get_vte_buffer_range()[0] + self.searchstring = searchtext + + self.reslabel.set_text(_("Searching scrollback")) + self.next.set_sensitive(True) + self.prev.set_sensitive(True) + self.next_search(None) + + def next_search(self, widget): + """Search forwards and jump to the next result, if any""" + startrow,endrow = self.get_vte_buffer_range() + while True: + if self.searchrow == endrow: + self.searchrow = startrow + self.reslabel.set_text(_('No more results')) + return + buffer = self.vte.get_text_range(self.searchrow, 0, + self.searchrow, -1, + self.search_character) + + index = buffer.find(self.searchstring) + if index != -1: + self.search_hit(self.searchrow) + self.searchrow += 1 + return + self.searchrow += 1 + + # FIXME: There is an issue in switching search direction, probably because + # we increment/decrement self.searchrow after each search iteration + def prev_search(self, widget): + """Jump back to the previous search""" + startrow,endrow = self.get_vte_buffer_range() + while True: + if self.searchrow == startrow: + self.searchrow = endrow + self.reslabel.set_text(_('No more results')) + return + buffer = self.vte.get_text_range(self.searchrow, 0, + self.searchrow, -1, + self.search_character) + + index = buffer.find(self.searchstring) + if index != -1: + self.search_hit(self.searchrow) + self.searchrow -= 1 + return + self.searchrow -= 1 + + def search_hit(self, row): + """Update the UI for a search hit""" + self.reslabel.set_text("%s %d" % (_('Found at row'), row)) + self.get_parent().scrollbar_jump(row) + self.next.show() + self.prev.show() + + def search_character(self, widget, col, row, junk): + """We have to have a callback for each character""" + return(True) + + def get_vte_buffer_range(self): + """Get the range of a vte widget""" + column, endrow = self.vte.get_cursor_position() + startrow = max(0, endrow - self.config['scrollback_lines']) + return(startrow, endrow) + + def end_search(self, widget=None): + """Trap and re-emit the end-search signal""" + self.searchrow = 0 + self.searchstring = None + self.reslabel.set_text('') + self.emit('end-search') + + def get_search_term(self): + """Return the currently set search term""" + return(self.entry.get_text()) + +gobject.type_register(Searchbar) diff --git a/terminatorlib/signalman.py b/terminatorlib/signalman.py new file mode 100755 index 00000000..f32f65d7 --- /dev/null +++ b/terminatorlib/signalman.py @@ -0,0 +1,57 @@ +#!/usr/bin/python +# Terminator by Chris Jones +# GPL v2 only +"""Simple management of Gtk Widget signal handlers""" + +from util import dbg, err + +class Signalman(object): + """Class providing glib signal tracking and management""" + + cnxids = None + + def __init__(self): + """Class initialiser""" + self.cnxids = {} + + def __del__(self): + """Class destructor. This is only used to check for stray signals""" + if len(self.cnxids.keys()) > 0: + err('Signals remain. This is likely a bug: %s' % self.cnxids) + + def new(self, widget, signal, handler, *args): + """Register a new signal on a widget""" + if not self.cnxids.has_key(widget): + dbg('creating new bucket for %s' % type(widget)) + self.cnxids[widget] = {} + + if self.cnxids[widget].has_key(signal): + err('%s already has a handler for %s' % (id(widget), signal)) + + self.cnxids[widget][signal] = widget.connect(signal, handler, *args) + dbg('connected %s::%s to %s' % (type(widget), signal, handler)) + + def remove_signal(self, widget, signal): + """Remove a signal handler""" + if not self.cnxids.has_key(widget): + dbg('%s is not registered' % widget) + return + if not self.cnxids[widget].has_key(signal): + dbg('%s not registered for %s' % (signal, type(widget))) + return + dbg('removing %s::%s' % (type(widget), signal)) + widget.disconnect(self.cnxids[widget][signal]) + del(self.cnxids[widget][signal]) + if len(self.cnxids[widget].keys()) == 0: + dbg('no more signals for widget') + del(self.cnxids[widget]) + + def remove_widget(self, widget): + """Remove all signal handlers for a widget""" + if not self.cnxids.has_key(widget): + dbg('%s not registered' % widget) + return + signals = self.cnxids[widget].keys() + for signal in signals: + self.remove_signal(widget, signal) + diff --git a/terminatorlib/terminal.py b/terminatorlib/terminal.py new file mode 100755 index 00000000..3e7c0db6 --- /dev/null +++ b/terminatorlib/terminal.py @@ -0,0 +1,1279 @@ +#!/usr/bin/python +# Terminator by Chris Jones +# GPL v2 only +"""terminal.py - classes necessary to provide Terminal widgets""" + +import sys +import os +import pygtk +pygtk.require('2.0') +import gtk +import gobject +import pango +import subprocess +import urllib + +from util import dbg, err, gerr, get_top_window +import util +from config import Config +from cwd import get_default_cwd +from terminator import Terminator +from titlebar import Titlebar +from terminal_popup_menu import TerminalPopupMenu +from searchbar import Searchbar +from translation import _ +from signalman import Signalman +import plugin + +try: + import vte +except ImportError: + gerr('You need to install python bindings for libvte') + sys.exit(1) + +# pylint: disable-msg=R0904 +class Terminal(gtk.VBox): + """Class implementing the VTE widget and its wrappings""" + + __gsignals__ = { + 'close-term': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + 'title-change': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, + (gobject.TYPE_STRING,)), + 'enumerate': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, + (gobject.TYPE_INT,)), + 'group-tab': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + 'ungroup-tab': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + 'ungroup-all': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + 'split-horiz': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + 'split-vert': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + 'tab-new': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + 'tab-top-new': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + 'focus-in': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + 'zoom': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + 'maximise': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + 'unzoom': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + 'resize-term': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, + (gobject.TYPE_STRING,)), + 'navigate': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, + (gobject.TYPE_STRING,)), + 'tab-change': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, + (gobject.TYPE_INT,)), + 'group-all': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + 'ungroup-all': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + 'group-tab': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + 'ungroup-tab': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + 'move-tab': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, + (gobject.TYPE_STRING,)), + } + + TARGET_TYPE_VTE = 8 + + terminator = None + vte = None + terminalbox = None + scrollbar = None + scrollbar_position = None + titlebar = None + searchbar = None + + group = None + cwd = None + command = None + clipboard = None + pid = None + + matches = None + config = None + default_encoding = None + custom_encoding = None + custom_font_size = None + + composite_support = None + + cnxids = None + + def __init__(self): + """Class initialiser""" + gtk.VBox.__init__(self) + self.__gobject_init__() + + self.terminator = Terminator() + self.terminator.register_terminal(self) + + self.connect('enumerate', self.terminator.do_enumerate) + self.connect('group-tab', self.terminator.group_tab) + self.connect('ungroup-tab', self.terminator.ungroup_tab) + self.connect('focus-in', self.terminator.focus_changed) + + self.matches = {} + self.cnxids = Signalman() + + self.config = Config() + + self.cwd = get_default_cwd() + self.clipboard = gtk.clipboard_get(gtk.gdk.SELECTION_CLIPBOARD) + + self.vte = vte.Terminal() + self.vte.set_size(80, 24) + self.vte._expose_data = None + if not hasattr(self.vte, "set_opacity") or \ + not hasattr(self.vte, "is_composited"): + self.composite_support = False + self.vte.show() + + self.default_encoding = self.vte.get_encoding() + self.update_url_matches(self.config['try_posix_regexp']) + + self.terminalbox = self.create_terminalbox() + + self.titlebar = Titlebar(self) + self.titlebar.connect_icon(self.on_group_button_press) + self.titlebar.connect('edit-done', self.on_edit_done) + self.connect('title-change', self.titlebar.set_terminal_title) + self.titlebar.connect('create-group', self.really_create_group) + + self.searchbar = Searchbar() + self.searchbar.connect('end-search', self.on_search_done) + + self.show() + self.pack_start(self.titlebar, False) + self.pack_start(self.terminalbox) + self.pack_end(self.searchbar) + + self.connect_signals() + + os.putenv('COLORTERM', 'gnome-terminal') + + env_proxy = os.getenv('http_proxy') + if not env_proxy: + if self.config['http_proxy'] and self.config['http_proxy'] != '': + os.putenv('http_proxy', self.config['http_proxy']) + + def set_profile(self, widget, profile): + """Set our profile""" + if profile != self.config.get_profile(): + self.config.set_profile(profile) + self.reconfigure() + + def get_profile(self): + """Return our profile name""" + return(self.config.profile) + + def close(self): + """Close ourselves""" + dbg('Terminal::close: emitting close-term') + self.emit('close-term') + + def create_terminalbox(self): + """Create a GtkHBox containing the terminal and a scrollbar""" + + terminalbox = gtk.HBox() + self.scrollbar = gtk.VScrollbar(self.vte.get_adjustment()) + self.scrollbar.set_no_show_all(True) + self.scrollbar_position = self.config['scrollbar_position'] + + if self.scrollbar_position not in ('hidden', 'disabled'): + self.scrollbar.show() + + if self.scrollbar_position == 'left': + func = terminalbox.pack_end + else: + func = terminalbox.pack_start + + func(self.vte) + func(self.scrollbar, False) + terminalbox.show() + + return(terminalbox) + + def update_url_matches(self, posix = True): + """Update the regexps used to match URLs""" + userchars = "-A-Za-z0-9" + passchars = "-A-Za-z0-9,?;.:/!%$^*&~\"#'" + hostchars = "-A-Za-z0-9" + pathchars = "-A-Za-z0-9_$.+!*(),;:@&=?/~#%'\"" + schemes = "(news:|telnet:|nntp:|file:/|https?:|ftps?:|webcal:)" + user = "[" + userchars + "]+(:[" + passchars + "]+)?" + urlpath = "/[" + pathchars + "]*[^]'.}>) \t\r\n,\\\"]" + + if posix: + dbg ('Terminal::update_url_matches: Trying POSIX URL regexps') + lboundry = "[[:<:]]" + rboundry = "[[:>:]]" + else: # GNU + dbg ('Terminal::update_url_matches: Trying GNU URL regexps') + lboundry = "\\<" + rboundry = "\\>" + + self.matches['full_uri'] = self.vte.match_add(lboundry + schemes + + "//(" + user + "@)?[" + hostchars +".]+(:[0-9]+)?(" + + urlpath + ")?" + rboundry + "/?") + + if self.matches['full_uri'] == -1: + if posix: + err ('Terminal::update_url_matches: POSIX failed, trying GNU') + self.update_url_matches(posix = False) + else: + err ('Terminal::update_url_matches: Failed adding URL matches') + else: + self.matches['voip'] = self.vte.match_add(lboundry + + '(callto:|h323:|sip:)' + "[" + userchars + "+][" + + userchars + ".]*(:[0-9]+)?@?[" + pathchars + "]+" + + rboundry) + self.matches['addr_only'] = self.vte.match_add (lboundry + + "(www|ftp)[" + hostchars + "]*\.[" + hostchars + + ".]+(:[0-9]+)?(" + urlpath + ")?" + rboundry + "/?") + self.matches['email'] = self.vte.match_add (lboundry + + "(mailto:)?[a-zA-Z0-9][a-zA-Z0-9.+-]*@[a-zA-Z0-9]" + + "[a-zA-Z0-9-]*\.[a-zA-Z0-9][a-zA-Z0-9-]+" + + "[.a-zA-Z0-9-]*" + rboundry) + self.matches['nntp'] = self.vte.match_add (lboundry + + """news:[-A-Z\^_a-z{|}~!"#$%&'()*+,./0-9;:=?`]+@""" + + "[-A-Za-z0-9.]+(:[0-9]+)?" + rboundry) + + # Now add any matches from plugins + try: + registry = plugin.PluginRegistry() + registry.load_plugins() + plugins = registry.get_plugins_by_capability('url_handler') + + for urlplugin in plugins: + name = urlplugin.handler_name + match = urlplugin.match + self.matches[name] = self.vte.match_add(match) + dbg('Terminal::update_matches: added plugin URL handler \ +for %s (%s)' % (name, urlplugin.__class__.__name__)) + except Exception, ex: + err('Terminal::update_url_matches: %s' % ex) + + def connect_signals(self): + """Connect all the gtk signals and drag-n-drop mechanics""" + + self.vte.connect('key-press-event', self.on_keypress) + self.vte.connect('button-press-event', self.on_buttonpress) + self.vte.connect('popup-menu', self.popup_menu) + + srcvtetargets = [("vte", gtk.TARGET_SAME_APP, self.TARGET_TYPE_VTE)] + dsttargets = [("vte", gtk.TARGET_SAME_APP, self.TARGET_TYPE_VTE), + ('text/plain', 0, 0), ('STRING', 0, 0), ('COMPOUND_TEXT', 0, 0)] + + for (widget, mask) in [ + (self.vte, gtk.gdk.CONTROL_MASK | gtk.gdk.BUTTON3_MASK), + (self.titlebar, gtk.gdk.BUTTON1_MASK)]: + widget.drag_source_set(mask, srcvtetargets, gtk.gdk.ACTION_MOVE) + + self.vte.drag_dest_set(gtk.DEST_DEFAULT_MOTION | + gtk.DEST_DEFAULT_HIGHLIGHT | gtk.DEST_DEFAULT_DROP, + dsttargets, gtk.gdk.ACTION_MOVE) + + for widget in [self.vte, self.titlebar]: + widget.connect('drag-begin', self.on_drag_begin, self) + widget.connect('drag-data-get', self.on_drag_data_get, + self) + + self.vte.connect('drag-motion', self.on_drag_motion, self) + self.vte.connect('drag-data-received', + self.on_drag_data_received, self) + + # FIXME: Shouldn't this be in configure()? + if self.config['copy_on_selection']: + self.cnxids.new(self.vte, 'selection-changed', + lambda widget: self.vte.copy_clipboard()) + + if self.composite_support: + self.vte.connect('composited-changed', + self.on_composited_changed) + + self.vte.connect('window-title-changed', lambda x: + self.emit('title-change', self.get_window_title())) + self.vte.connect('grab-focus', self.on_vte_focus) + self.vte.connect('focus-in-event', self.on_vte_focus_in) + self.vte.connect('size-allocate', self.on_vte_size_allocate) + + self.vte.add_events(gtk.gdk.ENTER_NOTIFY_MASK) + self.vte.connect('enter_notify_event', + self.on_vte_notify_enter) + + self.cnxids.new(self.vte, 'realize', self.reconfigure) + + def create_popup_group_menu(self, widget, event = None): + """Pop up a menu for the group widget""" + if event: + button = event.button + time = event.time + else: + button = 0 + time = 0 + + menu = self.populate_group_menu() + menu.show_all() + menu.popup(None, None, self.position_popup_group_menu, button, time, + widget) + return(True) + + def populate_group_menu(self): + """Fill out a group menu""" + menu = gtk.Menu() + groupitem = None + + item = gtk.MenuItem(_('New group...')) + item.connect('activate', self.create_group) + menu.append(item) + + if len(self.terminator.groups) > 0: + groupitem = gtk.RadioMenuItem(groupitem, _('None')) + groupitem.set_active(self.group == None) + groupitem.connect('activate', self.set_group, None) + menu.append(groupitem) + + for group in self.terminator.groups: + item = gtk.RadioMenuItem(groupitem, group, False) + item.set_active(self.group == group) + item.connect('toggled', self.set_group, group) + menu.append(item) + groupitem = item + + if self.group != None or len(self.terminator.groups) > 0: + menu.append(gtk.MenuItem()) + + if self.group != None: + item = gtk.MenuItem(_('Remove group %s') % self.group) + item.connect('activate', self.ungroup, self.group) + menu.append(item) + + if util.has_ancestor(self, gtk.Notebook): + item = gtk.MenuItem(_('G_roup all in tab')) + item.connect('activate', lambda x: self.emit('group_tab')) + menu.append(item) + + if len(self.terminator.groups) > 0: + item = gtk.MenuItem(_('Ungr_oup all in tab')) + item.connect('activate', lambda x: self.emit('ungroup_tab')) + menu.append(item) + + if len(self.terminator.groups) > 0: + item = gtk.MenuItem(_('Remove all groups')) + item.connect('activate', lambda x: self.emit('ungroup-all')) + menu.append(item) + + if self.group != None: + menu.append(gtk.MenuItem()) + + item = gtk.MenuItem(_('Close group %s') % self.group) + item.connect('activate', lambda x: + self.terminator.closegroupedterms(self.group)) + menu.append(item) + + menu.append(gtk.MenuItem()) + + groupitem = None + + for key, value in {_('Broadcast all'):'all', + _('Broadcast group'):'group', + _('Broadcast off'):'off'}.items(): + groupitem = gtk.RadioMenuItem(groupitem, key) + dbg('Terminal::populate_group_menu: %s active: %s' % + (key, self.terminator.groupsend == + self.terminator.groupsend_type[value])) + groupitem.set_active(self.terminator.groupsend == + self.terminator.groupsend_type[value]) + groupitem.connect('activate', self.set_groupsend, + self.terminator.groupsend_type[value]) + menu.append(groupitem) + + menu.append(gtk.MenuItem()) + + item = gtk.CheckMenuItem(_('Split to this group')) + item.set_active(self.config['split_to_group']) + item.connect('toggled', lambda x: self.do_splittogroup_toggle()) + menu.append(item) + + item = gtk.CheckMenuItem(_('Autoclean groups')) + item.set_active(self.config['autoclean_groups']) + item.connect('toggled', lambda x: self.do_autocleangroups_toggle()) + menu.append(item) + + menu.append(gtk.MenuItem()) + + item = gtk.MenuItem(_('Insert terminal number')) + item.connect('activate', lambda x: self.emit('enumerate', False)) + menu.append(item) + + item = gtk.MenuItem(_('Insert padded terminal number')) + item.connect('activate', lambda x: self.emit('enumerate', True)) + menu.append(item) + + return(menu) + + def position_popup_group_menu(self, menu, widget): + """Calculate the position of the group popup menu""" + screen_w = gtk.gdk.screen_width() + screen_h = gtk.gdk.screen_height() + + widget_win = widget.get_window() + widget_x, widget_y = widget_win.get_origin() + widget_w, widget_h = widget_win.get_size() + + menu_w, menu_h = menu.size_request() + + if widget_y + widget_h + menu_h > screen_h: + menu_y = max(widget_y - menu_h, 0) + else: + menu_y = widget_y + widget_h + + return(widget_x, menu_y, 1) + + def set_group(self, item, name): + """Set a particular group""" + if self.group == name: + # already in this group, no action needed + return + dbg('Terminal::set_group: Setting group to %s' % name) + self.group = name + self.titlebar.set_group_label(name) + self.terminator.group_hoover() + + def create_group(self, item): + """Trigger the creation of a group via the titlebar (because popup + windows are really lame)""" + self.titlebar.create_group() + + def really_create_group(self, widget, groupname): + """The titlebar has spoken, let a group be created""" + self.terminator.create_group(groupname) + self.set_group(None, groupname) + + def ungroup(self, widget, data): + """Remove a group""" + # FIXME: Could we emit and have Terminator do this? + for term in self.terminator.terminals: + if term.group == data: + term.set_group(None, None) + self.terminator.group_hoover() + + def set_groupsend(self, widget, value): + """Set the groupsend mode""" + # FIXME: Can we think of a smarter way of doing this than poking? + if value in self.terminator.groupsend_type.values(): + dbg('Terminal::set_groupsend: setting groupsend to %s' % value) + self.terminator.groupsend = value + + def do_splittogroup_toggle(self): + """Toggle the splittogroup mode""" + self.config['split_to_group'] = not self.config['split_to_group'] + + def do_autocleangroups_toggle(self): + """Toggle the autocleangroups mode""" + self.config['autoclean_groups'] = not self.config['autoclean_groups'] + + def reconfigure(self, widget=None): + """Reconfigure our settings""" + dbg('Terminal::reconfigure') + self.cnxids.remove_signal(self.vte, 'realize') + + # Handle child command exiting + self.cnxids.remove_signal(self.vte, 'child-exited') + + if self.config['exit_action'] == 'restart': + self.cnxids.new(self.vte, 'child-exited', self.spawn_child) + elif self.config['exit_action'] in ('close', 'left'): + self.cnxids.new(self.vte, 'child-exited', + lambda x: self.emit('close-term')) + + self.vte.set_emulation(self.config['emulation']) + if self.custom_encoding != True: + self.vte.set_encoding(self.config['encoding']) + self.vte.set_word_chars(self.config['word_chars']) + self.vte.set_mouse_autohide(self.config['mouse_autohide']) + + backspace = self.config['backspace_binding'] + delete = self.config['delete_binding'] + + # FIXME: This doesn't seem like we ever obey control-h or + # escape-sequence + try: + if backspace == 'ascii-del': + backbind = vte.ERASE_ASCII_BACKSPACE + else: + backbind = vte.ERASE_AUTO_BACKSPACE + except AttributeError: + if backspace == 'ascii-del': + backbind = 2 + else: + backbind = 1 + + try: + if delete == 'escape-sequence': + delbind = vte.ERASE_DELETE_SEQUENCE + else: + delbind = vte.ERASE_AUTO + except AttributeError: + if delete == 'escape-sequence': + delbind = 3 + else: + delbind = 0 + + self.vte.set_backspace_binding(backbind) + self.vte.set_delete_binding(delbind) + + if not self.custom_font_size: + try: + self.vte.set_font(pango.FontDescription(self.config['font'])) + except: + pass + self.vte.set_allow_bold(self.config['allow_bold']) + if self.config['use_theme_colors']: + fgcolor = self.vte.get_style().text[gtk.STATE_NORMAL] + bgcolor = self.vte.get_style().base[gtk.STATE_NORMAL] + else: + fgcolor = gtk.gdk.color_parse(self.config['foreground_color']) + bgcolor = gtk.gdk.color_parse(self.config['background_color']) + + colors = self.config['palette'].split(':') + palette = [] + for color in colors: + if color: + palette.append(gtk.gdk.color_parse(color)) + self.vte.set_colors(fgcolor, bgcolor, palette) + if self.config['cursor_color'] != '': + self.vte.set_color_cursor(gtk.gdk.color_parse(self.config['cursor_color'])) + if hasattr(self.vte, 'set_cursor_shape'): + self.vte.set_cursor_shape(getattr(vte, 'CURSOR_SHAPE_' + + self.config['cursor_shape'].upper())) + + background_type = self.config['background_type'] + if background_type == 'image' and \ + self.config['background_image'] is not None and \ + self.config['background_image'] != '': + self.vte.set_background_image_file(self.config['background_image']) + self.vte.set_scroll_background(self.config['scroll_background']) + else: + self.vte.set_background_image_file('') + self.vte.set_scroll_background(False) + + opacity = 65536 + if background_type in ('image', 'transparent'): + self.vte.set_background_tint_color(gtk.gdk.color_parse(self.config['background_color'])) + self.vte.set_background_saturation(1 - + (self.config['background_darkness'])) + opacity = int(self.config['background_darkness'] * 65536) + else: + self.vte.set_background_saturation(1) + + if self.composite_support: + self.vte.set_opacity(opacity) + if self.config['background_type'] == 'transparent': + self.vte.set_background_transparent(True) + + self.vte.set_cursor_blinks(self.config['cursor_blink']) + + if self.config['force_no_bell'] == True: + self.vte.set_audible_bell(False) + self.vte.set_visible_bell(False) + self.cnxids.remove_signal(self.vte, 'beep') + else: + self.vte.set_audible_bell(self.config['audible_bell']) + self.vte.set_visible_bell(self.config['visible_bell']) + self.cnxids.remove_signal(self.vte, 'beep') + if self.config['urgent_bell'] == True: + try: + self.cnxids.new(self.vte, 'beep', self.on_beep) + except TypeError: + err('beep signal unavailable with this version of VTE') + + self.vte.set_scrollback_lines(self.config['scrollback_lines']) + self.vte.set_scroll_on_keystroke(self.config['scroll_on_keystroke']) + self.vte.set_scroll_on_output(self.config['scroll_on_output']) + + if self.scrollbar_position != self.config['scrollbar_position']: + self.scrollbar_position = self.config['scrollbar_position'] + if self.config['scrollbar_position'] == 'disabled': + self.scrollbar.hide() + else: + self.scrollbar.show() + if self.config['scrollbar_position'] == 'left': + self.reorder_child(self.scrollbar, 0) + elif self.config['scrollbar_position'] == 'right': + self.reorder_child(self.vte, 0) + + if hasattr(self.vte, 'set_alternate_screen_scroll'): + self.vte.set_alternate_screen_scroll(self.config['alternate_screen_scroll']) + + self.titlebar.update() + self.vte.queue_draw() + + def get_window_title(self): + """Return the window title""" + return(self.vte.get_window_title() or str(self.command)) + + def on_group_button_press(self, widget, event): + """Handler for the group button""" + if event.button == 1: + self.create_popup_group_menu(widget, event) + return(False) + + def on_keypress(self, widget, event): + """Handler for keyboard events""" + if not event: + dbg('Terminal::on_keypress: Called on %s with no event' % widget) + return(False) + + # FIXME: Does keybindings really want to live in Terminator()? + mapping = self.terminator.keybindings.lookup(event) + + if mapping == "hide_window": + return(False) + + if mapping and mapping not in ['close_window', + 'full_screen', + 'new_tab']: + dbg('Terminal::on_keypress: lookup found: %r' % mapping) + # handle the case where user has re-bound copy to ctrl+ + # we only copy if there is a selection otherwise let it fall through + # to ^ + if (mapping == "copy" and event.state & gtk.gdk.CONTROL_MASK): + if self.vte.get_has_selection (): + getattr(self, "key_" + mapping)() + return(True) + else: + getattr(self, "key_" + mapping)() + return(True) + + # FIXME: This is all clearly wrong. We should be doing this better + # maybe we can emit the key event and let Terminator() care? + groupsend = self.terminator.groupsend + groupsend_type = self.terminator.groupsend_type + if groupsend != groupsend_type['off'] and self.vte.is_focus(): + if self.group and groupsend == groupsend_type['group']: + self.terminator.group_emit(self, self.group, 'key-press-event', + event) + if groupsend == groupsend_type['all']: + self.terminator.all_emit(self, 'key-press-event', event) + + return(False) + + def on_buttonpress(self, widget, event): + """Handler for mouse events""" + # Any button event should grab focus + widget.grab_focus() + + if event.button == 1: + # Ctrl+leftclick on a URL should open it + if event.state & gtk.gdk.CONTROL_MASK == gtk.gdk.CONTROL_MASK: + url = self.check_for_url(event) + if url: + self.open_url(url, prepare=True) + elif event.button == 2: + # middleclick should paste the clipboard + self.paste_clipboard(True) + return(True) + elif event.button == 3: + # rightclick should display a context menu if Ctrl is not pressed + if event.state & gtk.gdk.CONTROL_MASK == 0: + self.popup_menu(widget, event) + return(True) + + return(False) + + def popup_menu(self, widget, event=None): + """Display the context menu""" + menu = TerminalPopupMenu(self) + menu.show(widget, event) + + def do_scrollbar_toggle(self): + self.toggle_widget_visibility(self.scrollbar) + + def toggle_widget_visibility(self, widget): + if widget.get_property('visible'): + widget.hide() + else: + widget.show() + + def on_encoding_change(self, widget, encoding): + """Handle the encoding changing""" + current = self.vte.get_encoding() + if current != encoding: + dbg('on_encoding_change: setting encoding to: %s' % encoding) + self.custom_encoding = not (encoding == self.config['encoding']) + self.vte.set_encoding(encoding) + + def on_drag_begin(self, widget, drag_context, data): + """Handle the start of a drag event""" + widget.drag_source_set_icon_pixbuf(util.widget_pixbuf(self, 512)) + + def on_drag_data_get(self, widget, drag_context, selection_data, info, time, + data): + """I have no idea what this does, drag and drop is a mystery. sorry.""" + selection_data.set('vte', info, + str(data.terminator.terminals.index(self))) + + def on_drag_motion(self, widget, drag_context, x, y, time, data): + """*shrug*""" + if 'text/plain' in drag_context.targets: + # copy text from another widget + return + srcwidget = drag_context.get_source_widget() + if(isinstance(srcwidget, gtk.EventBox) and + srcwidget == self.titlebar) or widget == srcwidget: + # on self + return + + alloc = widget.allocation + rect = gtk.gdk.Rectangle(0, 0, alloc.width, alloc.height) + + if self.config['use_theme_colors']: + color = self.vte.get_style().text[gtk.STATE_NORMAL] + else: + color = gtk.gdk.color_parse(self.config['foreground_color']) + + pos = self.get_location(widget, x, y) + topleft = (0,0) + topright = (alloc.width,0) + topmiddle = (alloc.width/2,0) + bottomleft = (0, alloc.height) + bottomright = (alloc.width,alloc.height) + bottommiddle = (alloc.width/2, alloc.height) + middle = (alloc.width/2, alloc.height/2) + middleleft = (0, alloc.height/2) + middleright = (alloc.width, alloc.height/2) + #print "%f %f %d %d" %(coef1, coef2, b1,b2) + coord = () + if pos == "right": + coord = (topright, topmiddle, bottommiddle, bottomright) + elif pos == "top": + coord = (topleft, topright, middleright , middleleft) + elif pos == "left": + coord = (topleft, topmiddle, bottommiddle, bottomleft) + elif pos == "bottom": + coord = (bottomleft, bottomright, middleright , middleleft) + + #here, we define some widget internal values + widget._expose_data = { 'color': color, 'coord' : coord } + #redraw by forcing an event + connec = widget.connect_after('expose-event', self.on_expose_event) + widget.window.invalidate_rect(rect, True) + widget.window.process_updates(True) + #finaly reset the values + widget.disconnect(connec) + widget._expose_data = None + + def on_expose_event(self, widget, event): + """Handle an expose event while dragging""" + if not widget._expose_data: + return(False) + + color = widget._expose_data['color'] + coord = widget._expose_data['coord'] + + context = widget.window.cairo_create() + context.set_source_rgba(color.red, color.green, color.blue, 0.5) + if len(coord) > 0 : + context.move_to(coord[len(coord)-1][0],coord[len(coord)-1][1]) + for i in coord: + context.line_to(i[0],i[1]) + + context.fill() + return(False) + + def on_drag_data_received(self, widget, drag_context, x, y, selection_data, + info, time, data): + if selection_data.type == 'text/plain': + # copy text to destination + txt = selection_data.data.strip() + if txt[0:7] == 'file://': + text = "'%s'" % urllib.unquote(txt[7:]) + for term in self.terminator.get_target_terms(self): + term.feed(txt) + return + + widgetsrc = data.terminator.terminals[int(selection_data.data)] + srcvte = drag_context.get_source_widget() + #check if computation requireds + if (isinstance(srcvte, gtk.EventBox) and + srcvte == self.titlebar) or srcvte == widget: + return + + srchbox = widgetsrc + dsthbox = widget.get_parent().get_parent() + + dstpaned = dsthbox.get_parent() + srcpaned = srchbox.get_parent() + if isinstance(dstpaned, gtk.Window) and \ + isinstance(srcpaned, gtk.Window): + return + + pos = self.get_location(widget, x, y) + + srcpaned.remove(widgetsrc) + dstpaned.split_axis(dsthbox, pos in ['top', 'bottom'], widgetsrc) + srcpaned.hoover() + + def get_location(self, vte, x, y): + """Get our location within the terminal""" + pos = '' + #get the diagonales function for the receiving widget + coef1 = float(vte.allocation.height)/float(vte.allocation.width) + coef2 = -float(vte.allocation.height)/float(vte.allocation.width) + b1 = 0 + b2 = vte.allocation.height + #determine position in rectangle + """ + -------- + |\ /| + | \ / | + | \/ | + | /\ | + | / \ | + |/ \| + -------- + """ + if (x*coef1 + b1 > y ) and (x*coef2 + b2 < y ): + pos = "right" + if (x*coef1 + b1 > y ) and (x*coef2 + b2 > y ): + pos = "top" + if (x*coef1 + b1 < y ) and (x*coef2 + b2 > y ): + pos = "left" + if (x*coef1 + b1 < y ) and (x*coef2 + b2 < y ): + pos = "bottom" + return pos + + def grab_focus(self): + self.vte.grab_focus() + + def on_vte_focus(self, widget): + self.emit('title-change', self.get_window_title()) + + def on_vte_focus_out(self, widget, event): + return + + def on_vte_focus_in(self, widget, event): + self.emit('focus-in') + + def scrollbar_jump(self, position): + """Move the scrollbar to a particular row""" + self.scrollbar.set_value(position) + + def scrollbar_position(self): + """Return the current position of the scrollbar""" + return(self.scrollbar.get_value()) + + def on_search_done(self, widget): + """We've finished searching, so clean up""" + self.searchbar.hide() + self.scrollbar.set_value(self.vte.get_cursor_position()[1]) + self.vte.grab_focus() + + def on_edit_done(self, widget): + """A child widget is done editing a label, return focus to VTE""" + self.vte.grab_focus() + + def on_vte_size_allocate(self, widget, allocation): + self.titlebar.update_terminal_size(self.vte.get_column_count(), + self.vte.get_row_count()) + if self.vte.window and self.config['geometry_hinting']: + window = util.get_top_window(self) + window.set_rough_geometry_hints() + + def on_vte_notify_enter(self, term, event): + """Handle the mouse entering this terminal""" + if self.config['focus'] in ['sloppy', 'mouse']: + if self.titlebar.editing() == False: + term.grab_focus() + return(False) + + def get_zoom_data(self): + """Return a dict of information for Window""" + data = {} + data['old_font'] = self.vte.get_font() + data['old_char_height'] = self.vte.get_char_height() + data['old_char_width'] = self.vte.get_char_width() + data['old_allocation'] = self.vte.get_allocation() + data['old_padding'] = self.vte.get_padding() + data['old_columns'] = self.vte.get_column_count() + data['old_rows'] = self.vte.get_row_count() + data['old_parent'] = self.get_parent() + + return(data) + + def zoom_scale(self, widget, allocation, old_data): + """Scale our font correctly based on how big we are not vs before""" + self.cnxids.remove_signal(self, 'zoom') + + new_columns = self.vte.get_column_count() + new_rows = self.vte.get_row_count() + new_font = self.vte.get_font() + new_allocation = self.vte.get_allocation() + + old_alloc = {'x': old_data['old_allocation'].width - \ + old_data['old_padding'][0], + 'y': old_data['old_allocation'].height - \ + old_data['old_padding'][1] + } + + dbg('Terminal::zoom_scale: Resized from %dx%d to %dx%d' % ( + old_data['old_columns'], + old_data['old_rows'], + new_columns, + new_rows)) + + if new_rows == old_data['old_rows'] or \ + new_columns == old_data['old_columns']: + dbg('Terminal::zoom_scale: One axis unchanged, not scaling') + return + + old_area = old_data['old_columns'] * old_data['old_rows'] + new_area = new_columns * new_rows + area_factor = (new_area / old_area) / 2 + + new_font.set_size(old_data['old_font'].get_size() * area_factor) + self.vte.set_font(new_font) + + def is_zoomed(self): + """Determine if we are a zoomed terminal""" + prop = None + parent = self.get_parent() + window = get_top_window(self) + + try: + prop = window.get_property('term-zoomed') + except TypeError: + prop = False + + return(prop) + + def zoom(self, widget=None): + """Zoom ourself to fill the window""" + self.emit('zoom') + + def maximise(self, widget=None): + """Maximise ourself to fill the window""" + self.emit('maximise') + + def unzoom(self, widget=None): + """Restore normal layout""" + self.emit('unzoom') + + def spawn_child(self, widget=None): + update_records = self.config['update_records'] + login = self.config['login_shell'] + args = [] + shell = None + command = None + + self.vte.grab_focus() + + options = self.config.options_get() + if options.command: + command = options.command + options.command = None + elif options.execute: + command = options.execute + options.execute = None + elif self.config['use_custom_command']: + command = self.config['custom_command'] + + if type(command) is list: + shell = util.path_lookup(command[0]) + args = command + else: + shell = util.shell_lookup() + + if self.config['login_shell']: + args.insert(0, "-%s" % shell) + else: + args.insert(0, shell) + + if command is not None: + args += ['-c', command] + + if shell is None: + self.vte.feed(_('Unable to find a shell')) + return(-1) + + try: + os.putenv('WINDOWID', '%s' % self.vte.get_parent_window().xid) + except AttributeError: + pass + + dbg('Forking shell: "%s" with args: %s' % (shell, args)) + self.pid = self.vte.fork_command(command=shell, argv=args, envv=[], + loglastlog=login, logwtmp=update_records, + logutmp=update_records, directory=self.cwd) + self.command = shell + + self.titlebar.update() + + if self.pid == -1: + self.vte.feed(_('Unable to start shell:') + shell) + return(-1) + + def check_for_url(self, event): + """Check if the mouse is over a URL""" + return (self.vte.match_check(int(event.x / self.vte.get_char_width()), + int(event.y / self.vte.get_char_height()))) + + def prepare_url(self, urlmatch): + """Prepare a URL from a VTE match""" + url = urlmatch[0] + match = urlmatch[1] + + if match == self.matches['email'] and url[0:7] != 'mailto:': + url = 'mailto:' + url + elif match == self.matches['addr_only'] and url[0:3] == 'ftp': + url = 'ftp://' + url + elif match == self.matches['addr_only']: + url = 'http://' + url + elif match in self.matches.values(): + # We have a match, but it's not a hard coded one, so it's a plugin + try: + registry = plugin.PluginRegistry() + registry.load_plugins() + plugins = registry.get_plugins_by_capability('url_handler') + + for urlplugin in plugins: + if match == self.matches[urlplugin.handler_name]: + newurl = urlplugin.callback(url) + if newurl is not None: + dbg('Terminal::prepare_url: URL prepared by \ +%s plugin' % urlplugin.handler_name) + url = newurl + break; + except Exception, ex: + err('Terminal::prepare_url: %s' % ex) + + return(url) + + def open_url(self, url, prepare=False): + """Open a given URL, conditionally unpacking it from a VTE match""" + if prepare == True: + url = self.prepare_url(url) + dbg('open_url: URL: %s (prepared: %s)' % (url, prepare)) + gtk.show_uri(None, url, gtk.gdk.CURRENT_TIME) + + def paste_clipboard(self, primary=False): + """Paste one of the two clipboards""" + for term in self.terminator.get_target_terms(self): + if primary: + term.vte.paste_primary() + else: + term.vte.paste_clipboard() + self.vte.grab_focus() + + def feed(self, text): + """Feed the supplied text to VTE""" + self.vte.feed_child(text) + + def zoom_in(self): + """Increase the font size""" + self.zoom_font(True) + + def zoom_out(self): + """Decrease the font size""" + self.zoom_font(False) + + def zoom_font(self, zoom_in): + """Change the font size""" + pangodesc = self.vte.get_font() + fontsize = pangodesc.get_size() + + if fontsize > pango.SCALE and not zoom_in: + fontsize -= pango.SCALE + elif zoom_in: + fontsize += pango.SCALE + + pangodesc.set_size(fontsize) + self.vte.set_font(pangodesc) + self.custom_font_size = fontsize + + def zoom_orig(self): + """Restore original font size""" + dbg("Terminal::zoom_orig: restoring font to: %s" % self.config['font']) + self.vte.set_font(pango.FontDescription(self.config['font'])) + self.custom_font_size = None + + def get_cursor_position(self): + """Return the co-ordinates of our cursor""" + col, row = self.vte.get_cursor_position() + width = self.vte.get_char_width() + height = self.vte.get_char_height() + return((col * width, row * height)) + + def get_font_size(self): + """Return the width/height of our font""" + return((self.vte.get_char_width(), self.vte.get_char_height())) + + def get_size(self): + """Return the column/rows of the terminal""" + return((self.vte.get_column_count(), self.vte.get_row_count())) + + def on_beep(self, widget): + """Set the urgency hint for our window""" + window = util.get_top_window(self) + window.set_urgency_hint(True) + + # There now begins a great list of keyboard event handlers + # FIXME: Probably a bunch of these are wrong. TEST! + def key_zoom_in(self): + self.zoom_in() + + def key_zoom_out(self): + self.zoom_out() + + def key_copy(self): + self.vte.copy_clipboard() + + def key_paste(self): + self.vte.paste_clipboard() + + def key_toggle_scrollbar(self): + self.do_scrollbar_toggle() + + def key_zoom_normal(self): + self.zoom_orig () + + def key_search(self): + self.searchbar.start_search() + + # bindings that should be moved to Terminator as they all just call + # a function of Terminator. It would be cleaner if TerminatorTerm + # has absolutely no reference to Terminator. + # N (next) - P (previous) - O (horizontal) - E (vertical) - W (close) + def key_new_root_tab(self): + self.terminator.newtab (self, True) + + def key_cycle_next(self): + self.key_go_next() + + def key_cycle_prev(self): + self.key_go_prev() + + def key_go_next(self): + self.emit('navigate', 'next') + + def key_go_prev(self): + self.emit('navigate', 'prev') + + def key_go_up(self): + self.emit('navigate', 'up') + + def key_go_down(self): + self.emit('navigate', 'down') + + def key_go_left(self): + self.emit('navigate', 'left') + + def key_go_right(self): + self.emit('navigate', 'right') + + def key_split_horiz(self): + self.emit('split-horiz') + + def key_split_vert(self): + self.emit('split-vert') + + def key_close_term(self): + self.close() + + def key_resize_up(self): + self.emit('resize-term', 'up') + + def key_resize_down(self): + self.emit('resize-term', 'down') + + def key_resize_left(self): + self.emit('resize-term', 'left') + + def key_resize_right(self): + self.emit('resize-term', 'right') + + # FIXME: Nothing currently handles this signal. Make it so something does. + def key_move_tab_right(self): + self.emit('move-tab', 'right') + + # FIXME: Nothing currently handles this signal. Make it so something does. + def key_move_tab_left(self): + self.emit('move-tab', 'left') + + def key_toggle_zoom(self): + if self.is_zoomed(): + self.unzoom() + else: + self.maximise() + + def key_scaled_zoom(self): + if self.is_zoomed(): + self.unzoom() + else: + self.zoom() + + def key_next_tab(self): + self.emit('tab-change', -1) + + def key_prev_tab(self): + self.emit('tab-change', -2) + + def key_switch_to_tab_1(self): + self.emit('tab-change', 0) + + def key_switch_to_tab_2(self): + self.emit('tab-change', 1) + + def key_switch_to_tab_3(self): + self.emit('tab-change', 2) + + def key_switch_to_tab_4(self): + self.emit('tab-change', 3) + + def key_switch_to_tab_5(self): + self.emit('tab-change', 4) + + def key_switch_to_tab_6(self): + self.emit('tab-change', 5) + + def key_switch_to_tab_7(self): + self.emit('tab-change', 6) + + def key_switch_to_tab_8(self): + self.emit('tab-change', 7) + + def key_switch_to_tab_9(self): + self.emit('tab-change', 8) + + def key_switch_to_tab_10(self): + self.emit('tab-change', 9) + + def key_reset(self): + self.vte.reset (True, False) + + def key_reset_clear(self): + self.vte.reset (True, True) + + def key_group_all(self): + self.emit('group-all') + + def key_ungroup_all(self): + self.emit('ungroup-all') + + def key_group_tab(self): + self.emit('group-tab') + + def key_ungroup_tab(self): + self.emit('ungroup-tab') + + def key_new_window(self): + cmd = sys.argv[0] + + if not os.path.isabs(cmd): + # Command is not an absolute path. Figure out where we are + cmd = os.path.join (self.cwd, sys.argv[0]) + if not os.path.isfile(cmd): + # we weren't started as ./terminator in a path. Give up + err('Terminal::key_new_window: Unable to locate Terminator') + return False + + dbg("Terminal::key_new_window: Spawning: %s" % cmd) + subprocess.Popen([cmd,]) +# End key events + +gobject.type_register(Terminal) +# vim: set expandtab ts=4 sw=4: diff --git a/terminatorlib/terminal_popup_menu.py b/terminatorlib/terminal_popup_menu.py new file mode 100755 index 00000000..db19ae0d --- /dev/null +++ b/terminatorlib/terminal_popup_menu.py @@ -0,0 +1,241 @@ +#!/usr/bin/python +# Terminator by Chris Jones +# GPL v2 only +"""terminal_popup_menu.py - classes necessary to provide a terminal context +menu""" + +import gtk + +from version import APP_NAME +from translation import _ +from encoding import TerminatorEncoding +from util import err +from config import Config +from prefseditor import PrefsEditor +import plugin + +class TerminalPopupMenu(object): + """Class implementing the Terminal context menu""" + terminal = None + + def __init__(self, terminal): + """Class initialiser""" + self.terminal = terminal + + def show(self, widget, event=None): + """Display the context menu""" + terminal = self.terminal + + menu = gtk.Menu() + url = None + button = None + time = None + + if event: + url = terminal.check_for_url(event) + button = event.button + time = event.time + + if url: + if url[1] == terminal.matches['email']: + nameopen = _('_Send email to...') + namecopy = _('_Copy email address') + elif url[1] == terminal.matches['voip']: + nameopen = _('Ca_ll VoIP address') + namecopy = _('_Copy VoIP address') + else: + nameopen = _('_Open link') + namecopy = _('_Copy address') + + icon = gtk.image_new_from_stock(gtk.STOCK_JUMP_TO, + gtk.ICON_SIZE_MENU) + item = gtk.ImageMenuItem(nameopen) + item.set_property('image', icon) + item.connect('activate', lambda x: terminal.open_url(url, True)) + menu.append(item) + + item = gtk.MenuItem(namecopy) + item.connect('activate', + lambda x: terminal.clipboard.set_text(url[0])) + menu.append(item) + + menu.append(gtk.MenuItem()) + + item = gtk.ImageMenuItem(gtk.STOCK_COPY) + item.connect('activate', lambda x: terminal.vte.copy_clipboard()) + item.set_sensitive(terminal.vte.get_has_selection()) + menu.append(item) + + item = gtk.ImageMenuItem(gtk.STOCK_PASTE) + item.connect('activate', lambda x: terminal.paste_clipboard()) + menu.append(item) + + menu.append(gtk.MenuItem()) + + if not terminal.is_zoomed(): + item = gtk.ImageMenuItem('Split H_orizontally') + image = gtk.Image() + image.set_from_icon_name(APP_NAME + '_horiz', gtk.ICON_SIZE_MENU) + item.set_image(image) + if hasattr(item, 'set_always_show_image'): + item.set_always_show_image(True) + item.connect('activate', lambda x: terminal.emit('split-horiz')) + menu.append(item) + + item = gtk.ImageMenuItem('Split V_ertically') + image = gtk.Image() + image.set_from_icon_name(APP_NAME + '_vert', gtk.ICON_SIZE_MENU) + item.set_image(image) + if hasattr(item, 'set_always_show_image'): + item.set_always_show_image(True) + item.connect('activate', lambda x: terminal.emit('split-vert')) + menu.append(item) + + item = gtk.MenuItem(_('Open _Tab')) + item.connect('activate', lambda x: terminal.emit('tab-new')) + menu.append(item) + + menu.append(gtk.MenuItem()) + + item = gtk.ImageMenuItem(gtk.STOCK_CLOSE) + item.connect('activate', lambda x: terminal.emit('close-term')) + menu.append(item) + + menu.append(gtk.MenuItem()) + + if not terminal.is_zoomed(): + item = gtk.MenuItem(_('_Zoom terminal')) + item.connect('activate', terminal.zoom) + menu.append(item) + + item = gtk.MenuItem(_('Ma_ximise terminal')) + item.connect('activate', terminal.maximise) + menu.append(item) + + menu.append(gtk.MenuItem()) + else: + item = gtk.MenuItem(_('_Restore all terminals')) + item.connect('activate', terminal.unzoom) + menu.append(item) + + menu.append(gtk.MenuItem()) + + item = gtk.CheckMenuItem(_('Show _scrollbar')) + item.set_active(terminal.scrollbar.get_property('visible')) + item.connect('toggled', lambda x: terminal.do_scrollbar_toggle()) + menu.append(item) + + item = gtk.MenuItem(_('_Preferences')) + item.connect('activate', lambda x: PrefsEditor(self.terminal)) + menu.append(item) + + config = Config() + profilelist = config.list_profiles() + + if len(profilelist) > 1: + item = gtk.MenuItem(_('Profiles')) + submenu = gtk.Menu() + item.set_submenu(submenu) + menu.append(item) + + current = terminal.get_profile() + + group = None + + for profile in profilelist: + item = gtk.RadioMenuItem(group, profile.capitalize()) + if profile == current: + item.set_active(True) + item.connect('activate', terminal.set_profile, profile) + submenu.append(item) + + self.add_encoding_items(menu) + + try: + menuitems = [] + registry = plugin.PluginRegistry() + registry.load_plugins() + plugins = registry.get_plugins_by_capability('terminal_menu') + for menuplugin in plugins: + menuplugin.callback(menuitems, menu, terminal) + + if len(menuitems) > 0: + menu.append(gtk.MenuItem()) + + for menuitem in menuitems: + menu.append(menuitem) + except Exception, ex: + err('TerminalPopupMenu::show: %s' % ex) + + menu.show_all() + menu.popup(None, None, None, button, time) + + return(True) + + + def add_encoding_items(self, menu): + """Add the encoding list to the menu""" + terminal = self.terminal + active_encodings = terminal.config['active_encodings'] + item = gtk.MenuItem (_("Encodings")) + menu.append (item) + submenu = gtk.Menu () + item.set_submenu (submenu) + encodings = TerminatorEncoding ().get_list () + encodings.sort (lambda x, y: cmp (x[2].lower (), y[2].lower ())) + + current_encoding = terminal.vte.get_encoding () + group = None + + if current_encoding not in active_encodings: + active_encodings.insert (0, _(current_encoding)) + + for encoding in active_encodings: + if encoding == terminal.default_encoding: + extratext = " (%s)" % _("Default") + elif encoding == current_encoding and \ + terminal.custom_encoding == True: + extratext = " (%s)" % _("User defined") + else: + extratext = "" + + radioitem = gtk.RadioMenuItem (group, _(encoding) + extratext) + + if encoding == current_encoding: + radioitem.set_active (True) + + if group is None: + group = radioitem + + radioitem.connect ('activate', terminal.on_encoding_change, + encoding) + submenu.append (radioitem) + + item = gtk.MenuItem (_("Other Encodings")) + submenu.append (item) + #second level + + submenu = gtk.Menu () + item.set_submenu (submenu) + group = None + + for encoding in encodings: + if encoding[1] in active_encodings: + continue + + if encoding[1] is None: + label = "%s %s" % (encoding[2], terminal.vte.get_encoding ()) + else: + label = "%s %s" % (encoding[2], encoding[1]) + + radioitem = gtk.RadioMenuItem (group, label) + if group is None: + group = radioitem + + if encoding[1] == current_encoding: + radioitem.set_active (True) + + radioitem.connect ('activate', terminal.on_encoding_change, + encoding[1]) + submenu.append (radioitem) + diff --git a/terminatorlib/terminator.py b/terminatorlib/terminator.py index a9ac1432..da8bb7f8 100755 --- a/terminatorlib/terminator.py +++ b/terminatorlib/terminator.py @@ -1,1590 +1,264 @@ #!/usr/bin/python -# vim: tabstop=2 softtabstop=2 shiftwidth=2 expandtab -# Terminator - multiple gnome terminals in one window -# Copyright (C) 2006-2008 cmsj@tenshu.net -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, version 2 only. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# Terminator by Chris Jones +# GPL v2 only +"""terminator.py - class for the master Terminator singleton""" -"""Terminator by Chris Jones """ -import time, re, sys, os, platform +import gtk -import pygtk -pygtk.require ("2.0") -import gobject, gtk, pango +from borg import Borg +from config import Config +from keybindings import Keybindings +from util import dbg, err, get_top_window +import util -from terminatorlib.version import APP_NAME, APP_VERSION -from terminatorlib import config -from config import dbg, err, debug +class Terminator(Borg): + """master object for the application""" -from terminatorlib.keybindings import TerminatorKeybindings -from terminatorlib.terminatorterm import TerminatorTerm -from terminatorlib.prefs_profile import ProfileEditor -from terminatorlib import translation + windows = None + windowtitle = None + terminals = None + groups = None + config = None + keybindings = None -from terminatorlib.terminatoreditablelabel import TerminatorEditableLabel + groupsend = None + groupsend_type = {'all':0, 'group':1, 'off':2} -try: - import deskbar.core.keybinder as bindkey -except: - dbg (_("Unable to find python bindings for deskbar, "\ - "hide_window is not available.")) - pass + def __init__(self): + """Class initialiser""" -class TerminatorWindowTitle: - _window = None - text = None - _forced = False - _role = None + Borg.__init__(self, self.__class__.__name__) + self.prepare_attributes() - def __init__ (self, window): - self._window = window + def prepare_attributes(self): + """Initialise anything that isn't already""" - def set_title (self, newtext): - if not self._forced: - self.text = newtext - self.update () + if not self.windows: + self.windows = [] + if not self.terminals: + self.terminals = [] + if not self.groups: + self.groups = [] + if not self.groupsend: + self.groupsend = self.groupsend_type['group'] + if not self.config: + self.config = Config() + if not self.keybindings: + self.keybindings = Keybindings() + self.keybindings.configure(self.config['keybindings']) - def force_title (self, newtext): - if newtext: - self.set_title (newtext) - self._forced = True - else: - self._forced = False + def register_window(self, window): + """Register a new window widget""" + if window not in self.windows: + dbg('Terminator::register_window: registering %s:%s' % (id(window), + type(window))) + self.windows.append(window) - def update (self): - title = None + def deregister_window(self, window): + """de-register a window widget""" + dbg('Terminator::deregister_window: de-registering %s:%s' % + (id(window), type(window))) + self.windows.remove(window) + if len(self.windows) == 0: + # We have no windows left, we should exit + gtk.main_quit() - if self._forced: - title = self.text - else: - title = "%s" % self.text + def register_terminal(self, terminal): + """Register a new terminal widget""" + if terminal not in self.terminals: + dbg('Terminator::register_terminal: registering %s:%s' % + (id(terminal), type(terminal))) + self.terminals.append(terminal) + terminal.connect('ungroup-all', self.ungroup_all) + terminal.connect('navigate', self.navigate_terminal) + terminal.connect('tab-new', self.tab_new) - self._window.set_title (title) - -class TerminatorNotebookTabLabel(gtk.HBox): - _terminator = None - _notebook = None - _icon = None - _label = None - _button = None - - def __init__(self, title, notebook, terminator): - gtk.HBox.__init__(self, False) - self._notebook = notebook - self._terminator = terminator - - self._label = TerminatorEditableLabel(title) - self.update_angle() + def deregister_terminal(self, terminal): + """De-register a terminal widget""" + dbg('Terminator::deregister_terminal: de-registering %s:%s' % + (id(terminal), type(terminal))) + self.terminals.remove(terminal) - self.pack_start(self._label, True, True) - - self._icon = gtk.Image() - self._icon.set_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU) - - self.update_closebut() - - self.show_all() - - def update_closebut(self): - if self._terminator.conf.close_button_on_tab: - if not self._button: - self._button = gtk.Button() - self._button.set_relief(gtk.RELIEF_NONE) - self._button.set_focus_on_click(False) - self._button.set_relief(gtk.RELIEF_NONE) - self._button.add(self._icon) - self._button.connect('clicked', self.on_close) - self._button.set_name("terminator-tab-close-button") - self._button.connect("style-set", self.on_style_set) - if hasattr(self._button, "set_tooltip_text"): - self._button.set_tooltip_text(_("Close Tab")) - self.pack_start(self._button, False, False) - self.show_all() - else: - if self._button: - self._button.remove(self._icon) - self.remove(self._button) - del(self._button) - self._button = None - - def update_angle(self): - tab_pos = self._notebook.get_tab_pos() - if tab_pos == gtk.POS_LEFT: - self._label.set_angle(90) - elif tab_pos == gtk.POS_RIGHT: - self._label.set_angle(270) - else: - self._label.set_angle(0) - - def on_style_set(self, widget, prevstyle): - x, y = gtk.icon_size_lookup_for_settings( self._button.get_settings(), gtk.ICON_SIZE_MENU) - self._button.set_size_request(x + 2,y + 2) - - def on_close(self, widget): - nbpages = self._notebook.get_n_pages() - for i in xrange(0,nbpages): - if self._notebook.get_tab_label(self._notebook.get_nth_page(i)) == self: - #dbg("[Close from tab] Found tab at position [%d]" % i) - if not isinstance (self._notebook.get_nth_page(i), TerminatorTerm): - if self._terminator.confirm_close_multiple (self._terminator.window, _("tab")): - return False - term = self._terminator._notebook_first_term(self._notebook.get_nth_page(i)) - while term: - if term == self._notebook.get_nth_page(i): - self._terminator.closeterm(term) - break - self._terminator.closeterm(term) - term = self._terminator._notebook_first_term(self._notebook.get_nth_page(i)) - break - - def set_title(self, title, force=False): - self._label.set_text(title, force) - - def get_title(self): - return self._label.get_text() - - def height_request(self): - return self.size_request()[1] - - def width_request(self): - return self.size_request()[0] - - -class Terminator: - options = None - groupings = None - _urgency = False - origcwd = None - - def __init__ (self, profile = None, command = None, fullscreen = False, - maximise = False, borderless = False, no_gconf = False, - geometry = None, hidden = False, forcedtitle = None, role=None): - self.profile = profile - self.command = command - - self._zoomed = False - self._maximised = False - self._fullscreen = False - self._geometry = geometry - self.debugaddress = None - self.start_cwd = os.getcwd() - self._hidden = False - self.term_list = [] - self.gnome_client = None - self.groupsend = 1 # 0 off, 1 group (d), 2 all - self.splittogroup = 0 # 0 no group (d), 1 new takes orginators group - self.autocleangroups = 1 # 0 off, 1 on (d) - stores = [] - self.groupings = [] - - store = config.TerminatorConfValuestoreRC () - store.set_reconfigure_callback (self.reconfigure_vtes) - stores.append (store) - - self._tab_reorderable = True - if not hasattr(gtk.Notebook, "set_tab_reorderable") or not hasattr(gtk.Notebook, "get_tab_reorderable"): - self._tab_reorderable = False - - if not no_gconf: - try: - import gconf - if self.profile: - self.profile = gconf.escape_key (self.profile, -1) - store = config.TerminatorConfValuestoreGConf (self.profile) - store.set_reconfigure_callback (self.reconfigure_vtes) - dbg ('Terminator__init__: comparing %s and %s'%(self.profile, store.profile.split ('/').pop ())) - if self.profile == store.profile.split ('/').pop (): - # If we have been given a profile, and we loaded it, we should be higher priority than RC - dbg ('Terminator__init__: placing GConf before RC') - stores.insert (0, store) + if len(self.terminals) == 0: + for window in self.windows: + window.destroy() else: - stores.append (store) - except Exception, e: - # This should probably be ImportError; what else might it throw? - dbg("GConf setup threw exception %s" % str(e)) + dbg('Terminator::deregister_terminal: %d terminals remain' % + len(self.terminals)) - self.conf = config.TerminatorConfig (stores) + def reconfigure(self): + """Update configuration for the whole application""" - # Sort out cwd detection code, if available - self.pid_get_cwd = lambda pid: None - if platform.system() == 'FreeBSD': - try: - from terminatorlib import freebsd - self.pid_get_cwd = freebsd.get_process_cwd - dbg ('Using FreeBSD self.pid_get_cwd') - except (OSError, NotImplementedError, ImportError): - dbg ('FreeBSD version too old for self.pid_get_cwd') - elif platform.system() == 'Linux': - dbg ('Using Linux self.pid_get_cwd') - self.pid_get_cwd = lambda pid: os.path.realpath ('/proc/%s/cwd' % pid) - elif platform.system() == 'SunOS': - dbg ('Using SunOS self.pid_get_cwd') - self.pid_get_cwd = lambda pid: os.path.realpath ('/proc/%s/path/cwd' % pid) - else: - dbg ('Unable to set a self.pid_get_cwd, unknown system: %s' % platform.system) + if self.config['handle_size'] in xrange(0, 6): + gtk.rc_parse_string("""style "terminator-paned-style" { + GtkPaned::handle_size = %s } + class "GtkPaned" style "terminator-paned-style" """ % + self.config['handle_size']) + gtk.rc_reset_styles(gtk.settings_get_default()) - # import a library for viewing URLs - try: - dbg ('Trying to import gnome for X session and backup URL handling support') - global gnome - import gnome, gnome.ui - self.gnome_program = gnome.init(APP_NAME, APP_VERSION) - self.url_show = gnome.url_show - - # X session saving support - self.gnome_client = gnome.ui.master_client() - self.gnome_client.connect_to_session_manager() - self.gnome_client.connect('save-yourself', self.save_yourself) - self.gnome_client.connect('die', self.die) - except ImportError: - # webbrowser.open() is not really useful, but will do as a fallback - dbg ('gnome not available, no X session support, backup URL handling via webbrowser module') - import webbrowser - self.url_show = webbrowser.open + # Cause all the terminals to reconfigure + for terminal in self.terminals: + terminal.reconfigure() - self.icon_theme = gtk.IconTheme () + def tab_new(self, terminal): + """A terminal asked for a new tab. This function is an indirection + to the Window object""" + window = get_top_window(terminal) + window.tab_new() - self.keybindings = TerminatorKeybindings() - if self.conf.f11_modifier: - config.DEFAULTS['keybindings']['full_screen'] = 'F11' - print "Warning: Config setting f11_modifier is deprecated and will be removed in version 1.0" - print "Please add the following to the end of your terminator config:" - print "[keybindings]" - print "full_screen = F11" - self.keybindings.configure(self.conf.keybindings) + def navigate_terminal(self, terminal, direction): + """Nagivate around the terminals""" + current = self.terminals.index(terminal) + length = len(self.terminals) + next = None - self.set_handle_size (self.conf.handle_size) - self.set_closebutton_style () + if length <= 1: + return - self.window = gtk.Window () - if role: - self.window.set_role(role) + if direction == 'next': + next = current + 1 + if next >= length: + next = 0 + elif direction == 'prev': + next = current - 1 + if next < 0: + next = length - 1 + elif direction in ['left', 'right', 'up', 'down']: + window = get_top_window(terminal) + layout = window.get_visible_terminals() - self.windowtitle = TerminatorWindowTitle (self.window) - if forcedtitle: - self.windowtitle.force_title (forcedtitle) - self.windowtitle.update () + allocation = terminal.get_allocation() + possibles = [] - if self._geometry is not None: - dbg("Geometry=%s" % self._geometry) - if not self.window.parse_geometry(self._geometry): - err(_("Invalid geometry string %r") % self._geometry) + # Get the co-ordinate of the appropriate edge for this direction + edge = util.get_edge(allocation, direction) + # Find all visible terminals which are, in their entirity, in the + # direction we want to move + for term in layout: + rect = layout[term] + if util.get_nav_possible(edge, rect, direction): + possibles.append(term) - try: - self.window.set_icon (self.icon_theme.load_icon (APP_NAME, 48, 0)) - except: - self.icon = self.window.render_icon (gtk.STOCK_DIALOG_INFO, gtk.ICON_SIZE_BUTTON) - self.window.set_icon (self.icon) + if len(possibles) == 0: + return - self.window.connect ("key-press-event", self.on_key_press) - self.window.connect ("delete_event", self.on_delete_event) - self.window.connect ("destroy", self.on_destroy_event) - self.window.connect ("window-state-event", self.on_window_state_changed) + # Find out how far away each of the possible terminals is, then + # find the smallest distance. The winning terminals are all of + # those who are that distance away. + offsets = {} + for term in possibles: + rect = layout[term] + offsets[term] = util.get_nav_offset(edge, rect, direction) + keys = offsets.values() + keys.sort() + winners = [k for k, v in offsets.iteritems() if v == keys[0]] + next = self.terminals.index(winners[0]) - self.window.set_property ('allow-shrink', True) - - if fullscreen or self.conf.fullscreen: - self.fullscreen_toggle () - - if maximise or self.conf.maximise: - self.maximize () - - if borderless or self.conf.borderless: - self.window.set_decorated (False) - - # Set RGBA colormap if possible so VTE can use real alpha - # channels for transparency. - if self.conf.enable_real_transparency: - dbg ('H9TRANS: Enabling real transparency') - self.enable_rgba(True) - - # Start out with just one terminal - # FIXME: This should be really be decided from some kind of profile - term = (TerminatorTerm (self, self.profile, self.command)) - self.term_list = [term] - - self.window.add (term) - term._titlebox.hide() - self.window.show () - term.spawn_child () - self.save_yourself () - - couldbind = False - try: - couldbind = bindkey.tomboy_keybinder_bind(self.conf.keybindings['hide_window'],self.cbkeyCloak,term) - except: - pass - if couldbind: - if hidden or self.conf.hidden: - self.hide() - else: - if hidden or self.conf.hidden: - self.window.iconify() - - def on_term_resized(self): - win_total_width, win_total_height = self.window.get_size () - dbg ('Resized window is %dx%d' % (win_total_width, win_total_height)) - - # FIXME: find first terminal - firstidx = 0 - - # Walk terminals across top edge to sum column geometries - prev = -1 - column_sum = 0 - width_extra = 0 - walker = firstidx - while (walker != None): - term = self.term_list[walker] - font_width, font_height, columns, rows = term.get_size_details () - column_sum += columns - dbg ('Geometry hints (term %d) column += %d characters' % (walker, columns)) - prev = walker - walker = self._select_right (walker) - - # Walk terminals down left edge to sum row geometries - prev = -1 - row_sum = 0 - height_extra = 0 - walker = firstidx - while (walker != None): - term = self.term_list[walker] - font_width, font_height, columns, rows = term.get_size_details () - row_sum += rows - dbg ('Geometry hints (term %d) row += %d characters' % (walker, rows)) - prev = walker - walker = self._select_down (walker) - - # adjust... - width_extra = win_total_width - (column_sum * font_width) - height_extra = win_total_height - (row_sum * font_height) - - dbg ('Geometry hints based on font size: %dx%d, columns: %d, rows: %d, extra width: %d, extra height: %d' % (font_width, font_height, column_sum, row_sum, width_extra, height_extra)) - - self.window.set_geometry_hints(self.window, -1, -1, -1, -1, width_extra, height_extra, font_width, font_height, -1.0, -1.0) - - def set_handle_size (self, size): - if size in xrange (0,6): - gtk.rc_parse_string(""" - style "terminator-paned-style" { - GtkPaned::handle_size = %s - } + if len(winners) > 1: + # Break an n-way tie with the cursor position + cursor_x, cursor_y = terminal.get_cursor_position() + cursor_x = cursor_x + allocation.x + cursor_y = cursor_y + allocation.y + for term in winners: + rect = layout[term] + if util.get_nav_tiebreak(direction, cursor_x, cursor_y, rect): + next = self.terminals.index(term) + break; + else: + err('Unknown navigation direction: %s' % direction) - class "GtkPaned" style "terminator-paned-style" - """ % self.conf.handle_size) - - def set_closebutton_style (self): - gtk.rc_parse_string(""" - style "terminator-tab-close-button-style" { - GtkWidget::focus-padding = 0 - GtkWidget::focus-line-width = 0 - xthickness = 0 - ythickness = 0 - } - widget "*.terminator-tab-close-button" style "terminator-tab-close-button-style" - """) + if next is not None: + self.terminals[next].grab_focus() - def enable_rgba (self, rgba = False): - screen = self.window.get_screen() - if rgba: - colormap = screen.get_rgba_colormap() - else: - colormap = screen.get_rgb_colormap() - if colormap: - self.window.set_colormap(colormap) + def create_group(self, name): + """Create a new group""" + if name not in self.groups: + dbg('Terminator::create_group: registering group %s' % name) + self.groups.append(name) - def die(self, *args): - gtk.main_quit () + def ungroup_all(self, widget): + """Remove all groups""" + for terminal in self.terminals: + terminal.set_group(None, None) + self.groups = [] - def save_yourself (self, *args): - """ Save as much of our state as possible for the X session manager """ - dbg("Saving session for xsm") - args = [sys.argv[0], - ("--geometry=%dx%d" % self.window.get_size()) + ("+%d+%d" % self.window.get_position())] + def closegroupedterms(self, group): + """Close all terminals in a group""" + for terminal in self.terminals: + if terminal.group == group: + terminal.close() - # OptionParser should really help us out here - drop_next_arg = False - geompatt = re.compile(r'^--geometry(=.+)?') - for arg in sys.argv[1:]: - mo = geompatt.match(arg) - if mo: - if not mo.group(1): - drop_next_arg = True - elif not drop_next_arg and arg not in ('--maximise', '-m', '--fullscreen', '-f'): - args.append(arg) - drop_next_arg = False + def group_hoover(self): + """Clean out unused groups""" - if self._maximised: - args.append('--maximise') + if self.config['autoclean_groups']: + inuse = [] + todestroy = [] - if self._fullscreen: - args.append('--fullscreen') + for terminal in self.terminals: + if terminal.group: + if not terminal.group in inuse: + inuse.append(terminal.group) - if self.gnome_client: - # We can't set an interpreter because Gnome unconditionally spams it with - # --sm-foo-bar arguments before our own argument list. *mutter* - # So, hopefully your #! line is correct. If not, we could write out - # a shell script with the interpreter name etc. - c = self.gnome_client - c.set_program(sys.argv[0]) - dbg("Session restart command: %s with args %r in %s" % (sys.argv[0], args, self.start_cwd)) + for group in self.groups: + if not group in inuse: + todestroy.append(group) - c.set_restart_style(gnome.ui.RESTART_IF_RUNNING) - c.set_current_directory(self.start_cwd) - try: - c.set_restart_command(args) - c.set_clone_command(args) - except (TypeError,AttributeError): - # Apparantly needed for some Fedora systems - # see http://trac.nicfit.net/mesk/ticket/137 - dbg("Gnome bindings have weird set_clone/restart_command") - c.set_restart_command(len(args), args) - c.set_clone_command(len(args), args) - return True + dbg('Terminator::group_hoover: %d groups, hoovering %d' % + (len(self.groups), len(todestroy))) + for group in todestroy: + self.groups.remove(group) - def show(self): - """Show the terminator window""" - # restore window position - self.window.move(self.pos[0],self.pos[1]) - #self.window.present() - self.window.show_now() - self._hidden = False + def group_emit(self, terminal, group, type, event): + """Emit to each terminal in a group""" + dbg('Terminator::group_emit: emitting a keystroke for group %s' % + group) + for term in self.terminals: + if term != terminal and term.group == group: + term.vte.emit(type, event) - def hide(self): - """Hide the terminator window""" - # save window position - self.pos = self.window.get_position() - self.window.hide() - self._hidden = True + def all_emit(self, terminal, type, event): + """Emit to all terminals""" + for term in self.terminals: + if term != terminal: + term.vte.emit(type, event) - def cbkeyCloak(self, data): - """Callback event for show/hide keypress""" - if self._hidden: - self.show() - else: - self.hide() - - def maximize (self): - """ Maximize the Terminator window.""" - self.window.maximize () - - def unmaximize (self): - """ Unmaximize the Terminator window.""" - self.window.unmaximize () - - def fullscreen_toggle (self): - """ Toggle the fullscreen state of the window. If it is in - fullscreen state, it will be unfullscreened. If it is not, it - will be set to fullscreen state. - """ - if self._fullscreen: - self.window.unfullscreen () - else: - self.window.fullscreen () - - def fullscreen_absolute (self, fullscreen): - """ Explicitly set the fullscreen state of the window. - """ - if self._fullscreen != fullscreen: - self.fullscreen_toggle () - - def on_window_state_changed (self, window, event): - self._fullscreen = bool (event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN) - self._maximised = bool (event.new_window_state & gtk.gdk.WINDOW_STATE_MAXIMIZED) - dbg("window state changed: fullscreen: %s, maximised: %s" % (self._fullscreen, self._maximised)) - return (False) - - def on_delete_event (self, window, event, data=None): - if len (self.term_list) == 1: - return False - return self.confirm_close_multiple (window, _("window")) - - def confirm_close_multiple (self, window, type): - # show dialog - dialog = gtk.Dialog (_("Close?"), window, gtk.DIALOG_MODAL) - dialog.set_has_separator (False) - dialog.set_resizable (False) - - cancel = dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT) - close_all = dialog.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_ACCEPT) - label = close_all.get_children()[0].get_children()[0].get_children()[1].set_label(_("Close _Terminals")) - - primairy = gtk.Label (_('Close multiple terminals?')) - primairy.set_use_markup (True) - primairy.set_alignment (0, 0.5) - secundairy = gtk.Label (_("This %s has several terminals open. Closing the %s will also close all terminals within it.") % (type, type)) - secundairy.set_line_wrap(True) - primairy.set_alignment (0, 0.5) - - labels = gtk.VBox () - labels.pack_start (primairy, False, False, 6) - labels.pack_start (secundairy, False, False, 6) - - image = gtk.image_new_from_stock(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_DIALOG) - image.set_alignment (0.5, 0) - - box = gtk.HBox() - box.pack_start (image, False, False, 6) - box.pack_start (labels, False, False, 6) - dialog.vbox.pack_start (box, False, False, 12) - - dialog.show_all () - result = dialog.run () - dialog.destroy () - return not (result == gtk.RESPONSE_ACCEPT) - - def on_destroy_event (self, widget, data=None): - self.die() - - def on_beep (self, terminal): - self.set_urgency (True) - - def set_urgency (self, on): - if on == self._urgency: - return - - self._urgency = on - self.window.set_urgency_hint (on) - - # keybindings for the whole terminal window (affects the main - # windows containing the splited terminals) - def on_key_press (self, window, event): - """ Callback for the window to determine what to do with special - keys. Currently handled key-combo's: - * F11: toggle fullscreen state of the window. - * CTRL - SHIFT - Q: close all terminals - """ - self.set_urgency (False) - mapping = self.keybindings.lookup(event) - - if mapping: - dbg("on_key_press: lookup found %r" % mapping) - if mapping == 'full_screen': - self.fullscreen_toggle () - elif mapping == 'close_window': - if not self.on_delete_event (window, gtk.gdk.Event (gtk.gdk.DELETE)): - self.on_destroy_event (window, gtk.gdk.Event (gtk.gdk.DESTROY)) - else: - return (False) - return (True) - - def set_window_title(self, title): - """ - Modifies Terminator window title - """ - self.windowtitle.set_title(title) - - def add(self, widget, terminal, pos = "bottom"): - """ - Add a term to another at position pos - """ - if pos in ("top", "bottom"): - pane = gtk.VPaned() - vertical = True - elif pos in ("left", "right"): - pane = gtk.HPaned() - vertical = False - else: - err('Terminator.add: massive pos fail: %s' % pos) - return - - # get the parent of the provided terminal - parent = widget.get_parent () - - if isinstance (parent, gtk.Window): - # We have just one term - parent.remove(widget) - if pos in ("top", "left"): - pane.pack1 (terminal, True, True) - pane.pack2 (widget, True, True) - else: - pane.pack1 (widget, True, True) - pane.pack2 (terminal, True, True) - parent.add (pane) - - position = (vertical) and parent.allocation.height \ - or parent.allocation.width - - if (isinstance (parent, gtk.Notebook) or isinstance (parent, gtk.Window)) and widget.conf.titlebars: - #not the only term in the notebook/window anymore, need to reshow the title - widget._titlebox.update() - terminal._titlebox.update() - - if isinstance (parent, gtk.Notebook): - page = -1 - - for i in xrange(0, parent.get_n_pages()): - if parent.get_nth_page(i) == widget: - page = i - break - - label = parent.get_tab_label (widget) - widget.reparent (pane) - if pos in ("top", "left"): - pane.remove(widget) - pane.pack1 (terminal, True, True) - pane.pack2 (widget, True, True) - else: - pane.pack1 (widget, True, True) - pane.pack2 (terminal, True, True) - #parent.remove_page(page) - pane.show() - parent.insert_page(pane, None, page) - parent.set_tab_label(pane,label) - parent.set_tab_label_packing(pane, not self.conf.scroll_tabbar, not self.conf.scroll_tabbar, gtk.PACK_START) - if self._tab_reorderable: - parent.set_tab_reorderable(pane, True) - parent.set_current_page(page) - - position = (vertical) and parent.allocation.height \ - or parent.allocation.width - - if isinstance (parent, gtk.Paned): - # We are inside a split term - position = (vertical) and widget.allocation.height \ - or widget.allocation.width - - if (widget == parent.get_child1 ()): - widget.reparent (pane) - parent.pack1 (pane, True, True) - else: - widget.reparent (pane) - parent.pack2 (pane, True, True) - - if pos in ("top", "left"): - pane.remove(widget) - pane.pack1 (terminal, True, True) - pane.pack2 (widget, True, True) - else: - pane.pack1 (widget, True, True) - pane.pack2 (terminal, True, True) - - pane.pack1 (widget, True, True) - pane.pack2 (terminal, True, True) - - # show all, set position of the divider - pane.show () - pane.set_position (position / 2) - terminal.show () - - # insert the term reference into the list - index = self.term_list.index (widget) - if pos in ('bottom', 'right'): - index = index + 1 - self.term_list.insert (index, terminal) - # make the new terminal grab the focus - terminal._vte.grab_focus () - - return (terminal) - - def on_page_reordered(self, notebook, child, page_num): - #page has been reordered, we need to get the - # first term and last term - dbg ("Reordered: %d"%page_num) - nbpages = notebook.get_n_pages() - if nbpages == 1: - dbg("[ERROR] only one page in on_page_reordered") - - first = self._notebook_first_term(notebook.get_nth_page(page_num)) - last = self._notebook_last_term(notebook.get_nth_page(page_num)) - firstidx = self.term_list.index(first) - lastidx = self.term_list.index(last) - termslice = self.term_list[firstidx:lastidx+1] - #remove them from the list - for term in termslice: - self.term_list.remove(term) - - if page_num == 0: - #first page, we insert before the first term of next page - nexttab = notebook.get_nth_page(1) - sibling = self._notebook_first_term(nexttab) - siblingindex = self.term_list.index(sibling) - for term in termslice: - self.term_list.insert(siblingindex, term) - siblingindex += 1 - else: - #other pages, we insert after the last term of previous page - previoustab = notebook.get_nth_page(page_num - 1) - sibling = self._notebook_last_term(previoustab) - siblingindex = self.term_list.index(sibling) - for term in termslice: - siblingindex += 1 - self.term_list.insert(siblingindex, term) - - #for page reorder, we need to get the first term of a notebook - def notebook_first_term(self, notebook): - return self._notebook_first_term(notebook.get_nth_page(0)) - - def _notebook_first_term(self, child): - if isinstance(child, TerminatorTerm): - return child - elif isinstance(child, gtk.Paned): - return self._notebook_first_term(child.get_child1()) - elif isinstance(child, gtk.Notebook): - return self._notebook_first_term(child.get_nth_page(0)) - - dbg("[ERROR] unsupported class %s in _notebook_first_term" % child.__class__.__name__) - return None - - #for page reorder, we need to get the last term of a notebook - def notebook_last_term(self, notebook): - return self._notebook_last_term(notebook.get_nth_page(notebook.get_n_pages()-1)) - - def _notebook_last_term(self, child): - if isinstance(child, TerminatorTerm): - return child - elif isinstance(child, gtk.Paned): - return self._notebook_last_term(child.get_child2()) - elif isinstance(child, gtk.Notebook): - return self._notebook_last_term(child.get_nth_page(child.get_n_pages()-1)) - - dbg("[ERROR] unsupported class %s in _notebook_last_term" % child.__class__.__name__) - return None - - def newtab(self,widget, toplevel = False, command = None): - if self._zoomed: - # We don't want to add a new tab while we are zoomed in on a terminal - dbg ("newtab function called, but Terminator was in zoomed terminal mode.") - return - - terminal = TerminatorTerm (self, self.profile, command, widget.get_cwd()) - #only one term, we don't show the title - terminal._titlebox.hide() - if self.conf.extreme_tabs and not toplevel: - parent = widget.get_parent () - child = widget - else: - child = self.window.get_children()[0] - parent = child.get_parent() - - if isinstance(parent, gtk.Paned) or (isinstance(parent, gtk.Window) - and - ((self.conf.extreme_tabs and not toplevel) or not isinstance(child, gtk.Notebook))): - #no notebook yet. - notebook = gtk.Notebook() - if self._tab_reorderable: - notebook.connect('page-reordered',self.on_page_reordered) - notebook.set_tab_reorderable(widget, True) - notebook.set_property('homogeneous', not self.conf.scroll_tabbar) - notebook.set_scrollable (self.conf.scroll_tabbar) - # Config validates this. - pos = getattr(gtk, "POS_%s" % self.conf.tab_position.upper()) - notebook.set_tab_pos(pos) - notebook.set_show_tabs (not self.conf.hide_tabbar) - - if isinstance(parent, gtk.Paned): - if parent.get_child1() == child: - child.reparent(notebook) - parent.pack1(notebook) + def do_enumerate(self, widget, pad): + """Insert the number of each terminal in a group, into that terminal""" + if pad: + numstr = '%0'+str(len(str(len(self.terminals))))+'d' else: - child.reparent(notebook) - parent.pack2(notebook) - elif isinstance(parent, gtk.Window): - child.reparent(notebook) - parent.add(notebook) - if self._tab_reorderable: - notebook.set_tab_reorderable(child,True) - notebooklabel = "" - if isinstance(child, TerminatorTerm): - child._titlebox.hide() - if widget.get_window_title() is not None: - notebooklabel = widget.get_window_title() - notebooktablabel = TerminatorNotebookTabLabel(notebooklabel, notebook, self) - notebook.set_tab_label(child, notebooktablabel) - notebook.set_tab_label_packing(child, not self.conf.scroll_tabbar, not self.conf.scroll_tabbar, gtk.PACK_START) + numstr = '%d' - wal = self.window.allocation - if not (self._maximised or self._fullscreen): - self.window.resize(wal.width, - min(wal.height + notebooktablabel.height_request(), gtk.gdk.screen_height())) + for term in self.get_target_terms(widget): + idx = self.terminals.index(term) + term.feed(numstr % (idx + 1)) - notebook.show() - elif isinstance(parent, gtk.Notebook): - notebook = parent - elif isinstance(parent, gtk.Window) and isinstance(child, gtk.Notebook): - notebook = child - else: - return (False) - - ## NOTE - ## Here we need to append to the notebook before we can - ## spawn the terminal (WINDOW_ID needs to be set) - - notebook.append_page(terminal,None) - terminal.show () - terminal.spawn_child () - notebooklabel = terminal.get_window_title() - notebooktablabel = TerminatorNotebookTabLabel(notebooklabel, notebook, self) - notebook.set_tab_label(terminal, notebooktablabel) - notebook.set_tab_label_packing(terminal, not self.conf.scroll_tabbar, not self.conf.scroll_tabbar, gtk.PACK_START) - if self._tab_reorderable: - notebook.set_tab_reorderable(terminal,True) - ## Now, we set focus on the new term - notebook.set_current_page(-1) - terminal._vte.grab_focus () - - #adding a new tab, thus we need to get the - # last term of the previous tab and add - # the new term just after - sibling = self._notebook_last_term(notebook.get_nth_page(notebook.page_num(terminal)-1)) - index = self.term_list.index(sibling) - self.term_list.insert (index + 1, terminal) - return (True) - - def splitaxis (self, widget, vertical=True, command=None): - """ Split the provided widget on the horizontal or vertical axis. """ - if self._zoomed: - # We don't want to split the terminal while we are in zoomed mode - dbg ("splitaxis function called, but Terminator was in zoomed mode.") - return - - # create a new terminal and parent pane. - terminal = TerminatorTerm (self, self.profile, command, widget.get_cwd()) - if self.splittogroup: - terminal.set_group (None, widget._group) - pos = vertical and "right" or "bottom" - self.add(widget, terminal, pos) - terminal.show () - terminal.spawn_child () - - return - - def remove(self, widget, keep = False): - """Remove a TerminatorTerm from the Terminator view and terms list - Returns True on success, False on failure""" - parent = widget.get_parent () - sibling = None - focus_on_close = 'prev' - if isinstance (parent, gtk.Window): - # We are the only term - if not self.on_delete_event (parent, gtk.gdk.Event (gtk.gdk.DELETE)): - self.on_destroy_event (parent, gtk.gdk.Event (gtk.gdk.DESTROY)) - return True - - elif isinstance (parent, gtk.Paned): - index = self.term_list.index (widget) - grandparent = parent.get_parent () - - # Discover sibling while all objects exist - if widget == parent.get_child1 (): - sibling = parent.get_child2 () - focus_on_close = 'next' - if widget == parent.get_child2 (): - sibling = parent.get_child1 () - - if not sibling: - # something is wrong, give up - err ("Error: %s is not a child of %s"%(widget, parent)) - return False - - parent.remove(widget) - if isinstance(grandparent, gtk.Notebook): - page = -1 - for i in xrange(0, grandparent.get_n_pages()): - if grandparent.get_nth_page(i) == parent: - page = i - break - label = grandparent.get_tab_label (parent) - parent.remove(sibling) - grandparent.remove_page(page) - grandparent.insert_page(sibling, None,page) - grandparent.set_tab_label(sibling, label) - grandparent.set_tab_label_packing(sibling, not self.conf.scroll_tabbar, not self.conf.scroll_tabbar, gtk.PACK_START) - if self._tab_reorderable: - grandparent.set_tab_reorderable(sibling, True) - grandparent.set_current_page(page) - else: - grandparent.remove (parent) - sibling.reparent (grandparent) - if not self._zoomed: - grandparent.resize_children() - if isinstance(sibling, TerminatorTerm) and isinstance(sibling.get_parent(), gtk.Notebook): - sibling._titlebox.hide() - - self.term_list.remove (widget) - if not keep: - widget._vte.get_parent().remove(widget._vte) - widget._vte = None - - elif isinstance (parent, gtk.Notebook): - parent.remove(widget) - nbpages = parent.get_n_pages() - index = self.term_list.index (widget) - - self.term_list.remove (widget) - if not keep: - widget._vte.get_parent().remove(widget._vte) - widget._vte = None - if nbpages == 1: - if self.window.allocation.height != gtk.gdk.screen_height(): - self.window.resize(self.window.allocation.width, min(self.window.allocation.height - parent.get_tab_label(parent.get_nth_page(0)).height_request(), gtk.gdk.screen_height())) - sibling = parent.get_nth_page(0) - parent.remove(sibling) - gdparent = parent.get_parent() - if isinstance(gdparent, gtk.Window): - gdparent.remove(parent) - gdparent.add(sibling) - elif isinstance(gdparent, gtk.Paned): - if gdparent.get_child1() == parent: - gdparent.remove(parent) - gdparent.pack1(sibling) - else: - gdparent.remove(parent) - gdparent.pack2(sibling) - elif isinstance(gdparent, gtk.Notebook): - # extreme_tabs is on :( - label = gdparent.get_tab_label(parent) - gdparent.remove(parent) - gdparent.insert_page(sibling, None, 0) - gdparent.set_tab_label(sibling, label) - gdparent.set_tab_label_packing(sibling, not self.conf.scroll_tabbar, not self.conf.scroll_tabbar, gtk.PACK_START) - if self._tab_reorderable: - gdparent.set_tab_reorderable(sibling, True) - gdparent.set_current_page(0) + def get_target_terms(self, widget): + """Get the terminals we should currently be broadcasting to""" + if self.groupsend == self.groupsend_type['all']: + return(self.terminals) + elif self.groupsend == self.groupsend_type['group']: + termset = [] + for term in self.terminals: + if term == widget or (term.group != None and term.group == + widget.group): + termset.append(term) + return(termset) else: - err('Unknown grandparent of %s (parent is a notebook)' % widget) - if isinstance(sibling, TerminatorTerm) and sibling.conf.titlebars and sibling.conf.extreme_tabs: - sibling._titlebox.show() - else: - err('Attempting to remove terminal from unknown parent: %s' % parent) - if self.conf.focus_on_close == 'prev' or ( self.conf.focus_on_close == 'auto' and focus_on_close == 'prev'): - if index == 0: index = 1 - self.term_list[index - 1]._vte.grab_focus () - self._set_current_notebook_page_recursive(self.term_list[index - 1]) - elif self.conf.focus_on_close == 'next' or ( self.conf.focus_on_close == 'auto' and focus_on_close == 'next'): - if index == len(self.term_list): index = index - 1 - self.term_list[index]._vte.grab_focus () - self._set_current_notebook_page_recursive(self.term_list[index]) - - if len(self.term_list) == 1: - self.term_list[0]._titlebox.hide() - - return True - - def closeterm (self, widget): - if self._zoomed: - # We are zoomed, pop back out to normal layout before closing - dbg ("closeterm function called while in zoomed mode. Restoring previous layout before closing.") - self.toggle_zoom(widget, not self._maximised) - - if self.remove(widget): - self.group_hoover() - return True - return False - - def closegroupedterms (self, widget): - if self._zoomed: - # We are zoomed, pop back out to normal layout before closing - dbg ("closeterm function called while in zoomed mode. Restoring previous layout before closing.") - self.toggle_zoom(widget, not self._maximised) - - widget_group = widget._group - all_closed = True - for term in self.term_list[:]: - if term._group == widget_group and not self.remove(term): - all_closed = False - self.group_hoover() - return all_closed - - def go_to (self, term, selector): - current = self.term_list.index (term) - target = selector (term) - if not target is None: - term = self.term_list[target] - ##we need to set the current page of each notebook - self._set_current_notebook_page_recursive(term) - term._vte.grab_focus () - - def _select_direction (self, term, matcher): - '''Return index of terminal in given direction''' - # Handle either TerminatorTerm or int index - if type(term) == int: - current = term - term = self.term_list[current] - else: - current = self.term_list.index (term) - current_geo = term.get_geometry () - best_index = None - best_geo = None - - for i in range(0,len(self.term_list)): - if i == current: - continue - possible = self.term_list[i] - possible_geo = possible.get_geometry () - - #import pprint - #print "I am %d" % (current) - #pprint.pprint(current_geo) - #print "I saw %d" % (i) - #pprint.pprint(possible_geo) - - try: - if matcher (current_geo, possible_geo, best_geo): - best_index = i - best_geo = possible_geo - except TypeError, KeyError: - # Not being called on a Paned widget - pass - #if best_index is None: - # print "nothing best" - #else: - # print "sending %d" % (best_index) - return best_index - - def _match_up (self, current_geo, possible_geo, best_geo): - '''We want to find terminals that are fully above the top - border, but closest in the y direction, breaking ties via - the closest cursor x position.''' - if len(possible_geo.keys()) == 0: - dbg('_match_right: no possible geo, bailing') - return False - - #print "matching up..." - # top edge of the current terminal - edge = current_geo['origin_y'] - # botoom edge of the possible target - new_edge = possible_geo['origin_y']+possible_geo['span_y'] - - # Width of the horizontal bar that splits terminals - try: - horizontalBar = self.term_list[0].get_parent().style_get_property('handle-size') + self.term_list[0]._titlebox.get_allocation().height - except TypeError: - horizontalBar = 0 - # Vertical distance between two terminals - distance = current_geo['offset_y'] - (possible_geo['offset_y'] + possible_geo['span_y']) - if new_edge < edge: - #print "new_edge < edge" - if best_geo is None: - #print "first thing left" - return True - best_edge = best_geo['origin_y']+best_geo['span_y'] - if new_edge > best_edge and distance == horizontalBar: - #print "closer y" - return True - if new_edge == best_edge: - #print "same y" - - cursor = current_geo['origin_x'] + current_geo['cursor_x'] - new_cursor = possible_geo['origin_x'] + possible_geo['cursor_x'] - best_cursor = best_geo['origin_x'] + best_geo['cursor_x'] - - if abs(new_cursor - cursor) < abs(best_cursor - cursor): - #print "closer x" - return True - else: - if distance == horizontalBar: - return True - #print "fail" - return False - - def _match_down (self, current_geo, possible_geo, best_geo): - '''We want to find terminals that are fully below the bottom - border, but closest in the y direction, breaking ties via - the closest cursor x position.''' - if len(possible_geo.keys()) == 0: - dbg('_match_right: no possible geo, bailing') - return False - - #print "matching down..." - # bottom edge of the current terminal - edge = current_geo['origin_y']+current_geo['span_y'] - # top edge of the possible target - new_edge = possible_geo['origin_y'] - #print "edge: %d new_edge: %d" % (edge, new_edge) - - # Width of the horizontal bar that splits terminals - try: - horizontalBar = self.term_list[0].get_parent().style_get_property('handle-size') + self.term_list[0]._titlebox.get_allocation().height - except TypeError: - horizontalBar = 0 - # Vertical distance between two terminals - distance = possible_geo['offset_y'] - (current_geo['offset_y'] + current_geo['span_y']) - if new_edge > edge: - #print "new_edge > edge" - if best_geo is None: - #print "first thing right" - return True - best_edge = best_geo['origin_y'] - #print "best_edge: %d" % (best_edge) - if new_edge < best_edge and distance == horizontalBar: - #print "closer y" - return True - if new_edge == best_edge: - #print "same y" - - cursor = current_geo['origin_x'] + current_geo['cursor_x'] - new_cursor = possible_geo['origin_x'] + possible_geo['cursor_x'] - best_cursor = best_geo['origin_x'] + best_geo['cursor_x'] - - if abs(new_cursor - cursor) < abs(best_cursor - cursor): - #print "closer x" - return True - else: - if distance == horizontalBar: - return True - #print "fail" - return False - - def _match_left (self, current_geo, possible_geo, best_geo): - '''We want to find terminals that are fully to the left of - the left-side border, but closest in the x direction, breaking - ties via the closest cursor y position.''' - if len(possible_geo.keys()) == 0: - dbg('_match_right: no possible geo, bailing') - return False - - #print "matching left..." - # left-side edge of the current terminal - edge = current_geo['origin_x'] - # right-side edge of the possible target - new_edge = possible_geo['origin_x']+possible_geo['span_x'] - - # Width of the horizontal bar that splits terminals - try: - horizontalBar = self.term_list[0].get_parent().style_get_property('handle-size') + self.term_list[0]._titlebox.get_allocation().height - except TypeError: - horizontalBar = 0 - # Width of the vertical bar that splits terminals - if self.term_list[0].is_scrollbar_present(): - try: - verticalBar = self.term_list[0].get_parent().style_get_property('handle-size') + self.term_list[0].get_parent().style_get_property('scroll-arrow-vlength') - except TypeError: - horizontalBar = 0 - else: - try: - verticalBar = self.term_list[0].get_parent().style_get_property('handle-size') - except TypeError: - horizontalBar = 0 - # Horizontal distance between two terminals - distance = current_geo['offset_x'] - (possible_geo['offset_x'] + possible_geo['span_x']) - if new_edge <= edge: - #print "new_edge(%d) < edge(%d)" % (new_edge, edge) - if best_geo is None: - #print "first thing left" - return True - best_edge = best_geo['origin_x']+best_geo['span_x'] - if new_edge > best_edge and distance == verticalBar: - #print "closer x (new_edge(%d) > best_edge(%d))" % (new_edge, best_edge) - return True - if new_edge == best_edge: - #print "same x" - - cursor = current_geo['origin_y'] + current_geo['cursor_y'] - new_cursor = possible_geo['origin_y'] + possible_geo['cursor_y'] - best_cursor = best_geo['origin_y'] + best_geo['cursor_y'] - - if abs(new_cursor - cursor) < abs(best_cursor - cursor) and distance <> horizontalBar: - #print "closer y" - return True - #print "fail" - return False - - def _match_right (self, current_geo, possible_geo, best_geo): - '''We want to find terminals that are fully to the right of - the right-side border, but closest in the x direction, breaking - ties via the closest cursor y position.''' - if len(possible_geo.keys()) == 0: - dbg('_match_right: no possible geo, bailing') - return False - - #print "matching right..." - # right-side edge of the current terminal - edge = current_geo['origin_x']+current_geo['span_x'] - # left-side edge of the possible target - new_edge = possible_geo['origin_x'] - #print "edge: %d new_edge: %d" % (edge, new_edge) - - # Width of the horizontal bar that splits terminals - try: - horizontalBar = self.term_list[0].get_parent().style_get_property('handle-size') + self.term_list[0]._titlebox.get_allocation().height - except TypeError: - horizontalBar = 0 - # Width of the vertical bar that splits terminals - if self.term_list[0].is_scrollbar_present(): - try: - verticalBar = self.term_list[0].get_parent().style_get_property('handle-size') + self.term_list[0].get_parent().style_get_property('scroll-arrow-vlength') - except TypeError: - verticalBar = 0 - else: - try: - verticalBar = self.term_list[0].get_parent().style_get_property('handle-size') - except TypeError: - verticalBar = 0 - # Horizontal distance between two terminals - distance = possible_geo['offset_x'] - (current_geo['offset_x'] + current_geo['span_x']) - if new_edge >= edge: - #print "new_edge > edge" - if best_geo is None: - #print "first thing right" - return True - best_edge = best_geo['origin_x'] - #print "best_edge: %d" % (best_edge) - if new_edge < best_edge and distance == verticalBar: - #print "closer x" - return True - if new_edge == best_edge: - #print "same x" - - cursor = current_geo['origin_y'] + current_geo['cursor_y'] - new_cursor = possible_geo['origin_y'] + possible_geo['cursor_y'] - best_cursor = best_geo['origin_y'] + best_geo['cursor_y'] - - if abs(new_cursor - cursor) < abs(best_cursor - cursor) and distance <> horizontalBar: - #print "closer y" - return True - #print "fail" - return False - - def _select_up (self, term): - return self._select_direction (term, self._match_up) - - def _select_down (self, term): - return self._select_direction (term, self._match_down) - - def _select_left (self, term): - return self._select_direction (term, self._match_left) - - def _select_right (self, term): - return self._select_direction (term, self._match_right) - - def go_next (self, term): - self.go_to (term, self._select_next) - - def go_prev (self, term): - self.go_to (term, self._select_prev) - - def go_up (self, term): - self.go_to (term, self._select_up) - - def go_down (self, term): - self.go_to (term, self._select_down) - - def go_left (self, term): - self.go_to (term, self._select_left) - - def go_right (self, term): - self.go_to (term, self._select_right) - - def _select_next (self, term): - current = self.term_list.index (term) - next = None - if self.conf.cycle_term_tab: - notebookpage = self.get_first_notebook_page(term) - if notebookpage: - last = self._notebook_last_term(notebookpage[1]) - first = self._notebook_first_term(notebookpage[1]) - if term == last: - next = self.term_list.index(first) - - if next is None: - if current == len (self.term_list) - 1: - next = 0 - else: - next = current + 1 - return next - - def _select_prev (self, term): - current = self.term_list.index (term) - previous = None - if self.conf.cycle_term_tab: - notebookpage = self.get_first_notebook_page(term) - if notebookpage: - last = self._notebook_last_term(notebookpage[1]) - first = self._notebook_first_term(notebookpage[1]) - if term == first: - previous = self.term_list.index(last) - - if previous is None: - if current == 0: - previous = len (self.term_list) - 1 - else: - previous = current - 1 - return previous - - def _set_current_notebook_page_recursive(self, widget): - page = self.get_first_notebook_page(widget) - while page: - child = None - page_num = page[0].page_num(page[1]) - page[0].set_current_page(page_num) - page = self.get_first_notebook_page(page[0]) - - def resizeterm (self, widget, keyname): - if keyname in ('Up', 'Down'): - type = gtk.VPaned - elif keyname in ('Left', 'Right'): - type = gtk.HPaned - else: - err ("Invalid keytype: %s" % type) - return - - parent = self.get_first_parent_widget(widget, type) - if parent is None: - return - - #We have a corresponding parent pane - # - #allocation = parent.get_allocation() - - if keyname in ('Up', 'Down'): - maxi = parent.get_child1().get_allocation().height + parent.get_child2().get_allocation().height - 1 - - else: - maxi = parent.get_child1().get_allocation().width + parent.get_child2().get_allocation().width - 1 - move = 10 - if keyname in ('Up', 'Left'): - move = -10 - - move = max(2, parent.get_position() + move) - move = min(maxi, move) - - parent.set_position(move) - - def previous_tab(self, term): - notebook = self.get_first_parent_notebook(term) - if notebook: - cur = notebook.get_current_page() - pages = notebook.get_n_pages() - if cur == 0: - notebook.set_current_page(pages - 1) - else: - notebook.prev_page() - # This seems to be required in some versions of (py)gtk. - # Without it, the selection changes, but the displayed page doesn't change - # Seen in gtk-2.12.11 and pygtk-2.12.1 at least. - notebook.set_current_page(notebook.get_current_page()) - - def next_tab(self, term): - notebook = self.get_first_parent_notebook(term) - if notebook: - cur = notebook.get_current_page() - pages = notebook.get_n_pages() - if cur == pages - 1: - notebook.set_current_page(0) - else: - notebook.next_page() - notebook.set_current_page(notebook.get_current_page()) - - def switch_to_tab(self, term, index): - notebook = self.get_first_parent_notebook(term) - if notebook: - notebook.set_current_page(index) - notebook.set_current_page(notebook.get_current_page()) - - def move_tab(self, term, direction): - dbg("moving to direction %s" % direction) - data = self.get_first_notebook_page(term) - if data is None: - return False - (notebook, page) = data - page_num = notebook.page_num(page) - nbpages = notebook.get_n_pages() - #dbg ("%s %s %s %s" % (page_num, nbpages,notebook, page)) - if page_num == 0 and direction == 'left': - new_page_num = nbpages - elif page_num == nbpages - 1 and direction == 'right': - new_page_num = 0 - elif direction == 'left': - new_page_num = page_num - 1 - elif direction == 'right': - new_page_num = page_num + 1 - else: - dbg("[ERROR] unhandled combination in move_tab: direction = %s page_num = %d" % (direction, page_num)) - return False - notebook.reorder_child(page, new_page_num) - return True - - def get_first_parent_notebook(self, widget): - if isinstance (widget, gtk.Window): - return None - parent = widget.get_parent() - if isinstance (parent, gtk.Notebook): - return parent - return self.get_first_parent_notebook(parent) - - def get_first_parent_widget (self, widget, type): - """This method searches up through the gtk widget heirarchy - of 'widget' until it finds a parent widget of type 'type'""" - while not isinstance(widget.get_parent(), type): - widget = widget.get_parent() - if widget is None: - return widget - - return widget.get_parent() - - def get_first_notebook_page(self, widget): - if isinstance (widget, gtk.Window) or widget is None: - return None - parent = widget.get_parent() - if isinstance (parent, gtk.Notebook): - page = -1 - for i in xrange(0, parent.get_n_pages()): - if parent.get_nth_page(i) == widget: - return (parent, widget) - return self.get_first_notebook_page(parent) - - def reconfigure_vtes (self): - for term in self.term_list: - term.reconfigure_vte () - - def toggle_zoom(self, widget, fontscale = False): - if not self._zoomed: - widget._titlebars = widget._titlebox.get_property ('visible') - dbg ('toggle_zoom: not zoomed. remembered titlebar setting of %s'%widget._titlebars) - if widget._titlebars: - widget._titlebox.hide() - self.zoom_term (widget, fontscale) - else: - dbg ('toggle_zoom: zoomed. restoring titlebar setting of %s'%widget._titlebars) - self.unzoom_term (widget, True) - if widget._titlebars and \ - len(self.term_list) > 1 \ - and \ - (isinstance(widget, TerminatorTerm) and isinstance(widget.get_parent(),gtk.Paned))\ - : - widget._titlebox.show() - - widget._vte.grab_focus() - widget._titlebox.update() - - def zoom_term (self, widget, fontscale = False): - """Maximize to full window an instance of TerminatorTerm.""" - self.old_font = widget._vte.get_font () - self.old_char_height = widget._vte.get_char_height () - self.old_char_width = widget._vte.get_char_width () - self.old_allocation = widget._vte.get_allocation () - self.old_padding = widget._vte.get_padding () - self.old_columns = widget._vte.get_column_count () - self.old_rows = widget._vte.get_row_count () - self.old_parent = widget.get_parent() - - if isinstance(self.old_parent, gtk.Window): - return - if isinstance(self.old_parent, gtk.Notebook): - self.old_page = self.old_parent.get_current_page() - self.old_label = self.old_parent.get_tab_label (self.old_parent.get_nth_page (self.old_page)) - - self.window_child = self.window.get_children()[0] - self.window.remove(self.window_child) - self.old_parent.remove(widget) - self.window.add(widget) - self._zoomed = True - - if fontscale: - self.cnid = widget.connect ("size-allocate", self.zoom_scale_font) - else: - self._maximised = True - - widget._vte.grab_focus () - - def zoom_scale_font (self, widget, allocation): - # Disconnect ourself so we don't get called again - widget.disconnect (self.cnid) - - new_columns = widget._vte.get_column_count () - new_rows = widget._vte.get_row_count () - new_font = widget._vte.get_font () - new_allocation = widget._vte.get_allocation () - - old_alloc = { 'x': self.old_allocation.width - self.old_padding[0], - 'y': self.old_allocation.height - self.old_padding[1] }; - - dbg ('zoom_scale_font: I just went from %dx%d to %dx%d.'%(self.old_columns, self.old_rows, new_columns, new_rows)) - - if (new_rows == self.old_rows) or (new_columns == self.old_columns): - dbg ('zoom_scale_font: At least one of my axes didn not change size. Refusing to zoom') - return - - old_char_spacing = old_alloc['x'] - (self.old_columns * self.old_char_width) - old_line_spacing = old_alloc['y'] - (self.old_rows * self.old_char_height) - dbg ('zoom_scale_font: char. %d = %d - (%d * %d)' % (old_char_spacing, old_alloc['x'], self.old_columns, self.old_char_width)) - dbg ('zoom_scale_font: lines. %d = %d - (%d * %d)' % (old_line_spacing, old_alloc['y'], self.old_rows, self.old_char_height)) - dbg ('zoom_scale_font: Previously my char spacing was %d and my row spacing was %d' % (old_char_spacing, old_line_spacing)) - - old_area = self.old_columns * self.old_rows - new_area = new_columns * new_rows - area_factor = new_area / old_area - dbg ('zoom_scale_font: My area changed from %d characters to %d characters, a factor of %f.'%(old_area, new_area, area_factor)) - - dbg ('zoom_scale_font: Post-scale-factor, char spacing should be %d and row spacing %d' % (old_char_spacing * (area_factor/2), old_line_spacing * (area_factor/2))) - dbg ('zoom_scale_font: char width should be %d, it was %d' % ((new_allocation.width - (old_char_spacing * (area_factor / 2)))/self.old_columns, self.old_char_width)) - dbg ('zoom_scale_font: char height should be %d, it was %d' % ((new_allocation.height - (old_line_spacing * (area_factor / 2)))/self.old_rows, self.old_char_height)) - - new_char_width = (new_allocation.width - (old_char_spacing * (area_factor / 2)))/self.old_columns - new_char_height = (new_allocation.height - (old_line_spacing * (area_factor / 2)))/self.old_rows - font_scaling_factor = min (float(new_char_width) / float(self.old_char_width), float(new_char_height) / float(self.old_char_height)) - - new_font_size = self.old_font.get_size () * font_scaling_factor * 0.9 - if new_font_size < self.old_font.get_size (): - dbg ('zoom_scale_font: new font size would have been smaller. bailing.') - return - - new_font.set_size (new_font_size) - dbg ('zoom_scale_font: Scaled font from %f to %f'%(self.old_font.get_size () / pango.SCALE, new_font.get_size () / pango.SCALE)) - widget._vte.set_font (new_font) - - def unzoom_term (self, widget, fontscale = False): - """Proof of concept: Go back to previous application - widget structure. - """ - if self._zoomed: - if fontscale: - widget._vte.set_font (self.old_font) - self._zoomed = False - self._maximised = False - - self.window.remove(widget) - self.window.add(self.window_child) - if isinstance(self.old_parent, gtk.Notebook): - self.old_parent.insert_page(widget, None, self.old_page) - self.old_parent.set_tab_label(widget, self.old_label) - self.old_parent.set_tab_label_packing(widget, not self.conf.scroll_tabbar, not self.conf.scroll_tabbar, gtk.PACK_START) - if self._tab_reorderable: - self.old_parent.set_tab_reorderable(widget, True) - self.old_parent.set_current_page(self.old_page) - - else: - self.old_parent.add(widget) - - widget._vte.grab_focus () - - def edit_profile (self, widget): - if not self.options: - self.options = ProfileEditor(self) - self.options.go() - - def group_emit (self, terminatorterm, group, type, event): - for term in self.term_list: - if term != terminatorterm and term._group == group: - term._vte.emit (type, event) - - def all_emit (self, terminatorterm, type, event): - for term in self.term_list: - if term != terminatorterm: - term._vte.emit (type, event) - - def group_hoover (self): - if self.autocleangroups: - destroy = [] - for group in self.groupings: - save = False - for term in self.term_list: - if term._group == group: - save = True - - if not save: - destroy.append (group) - - for group in destroy: - self.groupings.remove (group) + return([widget]) + + def group_tab(self, widget): + """Group all the terminals in a tab""" + pass + + def ungroup_tab(self, widget): + """Ungroup all the terminals in a tab""" + pass + + def focus_changed(self, widget): + """We just moved focus to a new terminal""" + for terminal in self.terminals: + terminal.titlebar.update(widget) + return +# vim: set expandtab ts=4 sw=4: diff --git a/terminatorlib/terminatorterm.py b/terminatorlib/terminatorterm.py deleted file mode 100755 index 5bbe2617..00000000 --- a/terminatorlib/terminatorterm.py +++ /dev/null @@ -1,1942 +0,0 @@ -#!/usr/bin/python -# Terminator - multiple gnome terminals in one window -# Copyright (C) 2006-2008 cmsj@tenshu.net -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, version 2 only. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -"""Terminator by Chris Jones """ -import pygtk -pygtk.require ("2.0") -import gobject, gtk, pango -import os, signal, sys, subprocess, pwd, re, urllib2 - -#import version details -from terminatorlib.version import * - -# import our configuration loader -from terminatorlib import config -from terminatorlib.config import dbg, err, debug - -#import encoding list -from terminatorlib.encoding import TerminatorEncoding -from terminatorlib.terminatoreditablelabel import TerminatorEditableLabel -# import translation support -from terminatorlib import translation - -# import vte-bindings -try: - import vte -except ImportError: - error = gtk.MessageDialog (None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, - _('You need to install python bindings for libvte ("python-vte" in debian/ubuntu)')) - error.run() - sys.exit (1) - -class TerminatorTermTitle (gtk.EventBox): - wanted = None - _title = None - _termtext = "" - _sizetext = "" - _group = None - _separator = None - _hbox = None - _ebox = None - _grouphbox = None - _icon = None - _parent = None - _unzoomed_title = None - _terminal = None - terminator = None - - def __init__ (self, terminal, terminator, configwanted = False): - gtk.EventBox.__init__ (self) - - self._title = TerminatorEditableLabel() - self._group = gtk.Label () - self._separator = gtk.VSeparator () - self._ebox = gtk.EventBox () - self._grouphbox = gtk.HBox () - self._icon = gtk.Image () - self._hbox = gtk.HBox () - self._terminal = terminal - - self.terminator = terminator - if self.terminator.groupsend == 2: - self.set_from_icon_name (APP_NAME + \ - '_active_broadcast_all', gtk.ICON_SIZE_MENU) - elif self.terminator.groupsend == 1: - self.set_from_icon_name (APP_NAME + \ - '_active_broadcast_group', gtk.ICON_SIZE_MENU) - else: - self.set_from_icon_name (APP_NAME + \ - '_active_broadcast_off', gtk.ICON_SIZE_MENU) - - self._grouphbox.pack_start (self._icon, False, True, 2) - self._grouphbox.pack_start (self._group, False, True, 2) - self._ebox.add (self._grouphbox) - self._ebox.show_all () - - self._hbox.pack_start (self._ebox, False, True, 0) - self._hbox.pack_start (self._separator, False, True, 0) - self._hbox.pack_start (self._title, True, True) - self.add (self._hbox) - - self._title.show_all () - self._hbox.show () - - self.wanted = configwanted - - self.connect ("button-press-event", self.on_clicked) - - def connect_icon (self, func): - self._ebox.connect ("button-release-event", func) - - def on_clicked (self, widget, event): - if self._parent is not None: - self._parent._vte.grab_focus () - - def set_group_label (self, name): - """If 'name' is None, hide the group name object, otherwise set it as the group label""" - if name: - self._group.set_text (name) - self._group.show () - else: - self._group.hide () - self._separator.show () - - def set_terminal_title (self, name): - """Set the title text shown in the titlebar""" - self._termtext = name - self.update_label () - - def set_terminal_size (self, width, height): - """Set the terminal size shown in the titlebar""" - self._sizetext = "%sx%s" % (width, height) - self.update_label () - - def update_label (self): - """Update the gtk label with values previously set""" - self._title.set_text ("%s %s" % (self._termtext, self._sizetext)) - - def get_terminal_title (self): - """Return the text showin in the titlebar""" - return (self._termtext) - - def set_from_icon_name (self, name, size = gtk.ICON_SIZE_MENU): - """Set an icon for the group label""" - if not name: - self._icon.hide () - return - - self._icon.set_from_icon_name (APP_NAME + name, size) - self._icon.show () - - def update_colors(self, source): - """Update terminals titlebar colours based on grouping""" - term = self._terminal - if term != source and term._group != None and term._group == source._group: - # Not active, group is not none, and in active's group - if self.terminator.groupsend == 0: - title_fg = term.conf.title_ia_txt_color - title_bg = term.conf.title_ia_bg_color - icon = '_receive_off' - else: - title_fg = term.conf.title_rx_txt_color - title_bg = term.conf.title_rx_bg_color - icon = '_receive_on' - group_fg = term.conf.title_rx_txt_color - group_bg = term.conf.title_rx_bg_color - elif term != source and term._group == None or term._group != source._group: - # Not active, group is not none, not in active's group - if self.terminator.groupsend == 2: - title_fg = term.conf.title_rx_txt_color - title_bg = term.conf.title_rx_bg_color - icon = '_receive_on' - else: - title_fg = term.conf.title_ia_txt_color - title_bg = term.conf.title_ia_bg_color - icon = '_receive_off' - group_fg = term.conf.title_ia_txt_color - group_bg = term.conf.title_ia_bg_color - else: - title_fg = term.conf.title_tx_txt_color - title_bg = term.conf.title_tx_bg_color - if self.terminator.groupsend == 2: - icon = '_active_broadcast_all' - elif self.terminator.groupsend == 1: - icon = '_active_broadcast_group' - else: - icon = '_active_broadcast_off' - group_fg = term.conf.title_tx_txt_color - group_bg = term.conf.title_tx_bg_color - - self._title.modify_fg (gtk.STATE_NORMAL, gtk.gdk.color_parse (title_fg)) - self._group.modify_fg (gtk.STATE_NORMAL, gtk.gdk.color_parse (group_fg)) - self.modify_bg (gtk.STATE_NORMAL, gtk.gdk.color_parse (title_bg)) - self._ebox.modify_bg (gtk.STATE_NORMAL, gtk.gdk.color_parse (group_bg)) - self.set_from_icon_name(icon, gtk.ICON_SIZE_MENU) - return - - def update (self): - """Update our state""" - if not self._parent: - self._parent = self.get_parent () - - if self._parent.terminator._zoomed and len (self._parent.terminator.term_list): - if not self._unzoomed_title: - self._unzoomed_title = self.get_terminal_title () - if self._parent.conf.zoomedtitlebar: - self.set_terminal_title ("Zoomed/Maximised terminal, %d hidden" % (len (self._parent.terminator.term_list) - 1)) - self.show() - else: - self.hide() - return - else: - if self._unzoomed_title: - self.set_terminal_title (self._unzoomed_title) - self._unzoomed_title = None - - if isinstance (self._parent.get_parent (), gtk.Window): - self.hide() - return - - if (self._parent.conf.titlebars and self.wanted) or self._parent._group: - self.show () - else: - self.hide () - - if self._parent._group: - self.set_group_label (self._parent._group) - else: - self.set_group_label (None) - -class TerminatorTerm (gtk.VBox): - - matches = None - TARGET_TYPE_VTE = 8 - _custom_font_size = None - _custom_encoding = None - _default_encoding = None - _group = None - focus = None - _urgent_bell_cnid = None - - def __init__ (self, terminator, profile = None, command = None, cwd = None): - gtk.VBox.__init__ (self) - self.terminator = terminator - self.conf = terminator.conf - self.command = command - self._oldtitle = "" - self.matches = {} - - self.cwd = cwd or os.getcwd(); - if not os.path.exists(self.cwd) or not os.path.isdir(self.cwd): - self.cwd = pwd.getpwuid(os.getuid ())[5] - - self.clipboard = gtk.clipboard_get (gtk.gdk.SELECTION_CLIPBOARD) - self.scrollbar_position = self.conf.scrollbar_position - - self._composited_support = True - self._vte = vte.Terminal () - self._default_encoding = self._vte.get_encoding() - if not hasattr(self._vte, "set_opacity") or not hasattr(self._vte, "is_composited"): - self._composited_support = False - dbg ('H9TRANS: composited_support: %s' % self._composited_support) - #self._vte.set_double_buffered(True) - self._vte.set_size (80, 24) - self._vte._expose_data = None - self._vte.show () - - self._termbox = gtk.HBox () - self._termbox.show() - - self._titlebox = TerminatorTermTitle (self, self.terminator, self.conf.titlebars) - - self._search_string = None - self._searchbox = gtk.HBox() - self._searchinput = gtk.Entry() - self._searchinput.set_activates_default(True) - self._searchinput.show() - - self._searchinput.connect('activate', self.do_search) - self._searchinput.connect('key-press-event', self.search_keypress) - - slabel = gtk.Label() - slabel.set_text(_("Search:")) - slabel.show() - - sclose = gtk.Button() - sclose.set_relief(gtk.RELIEF_NONE) - sclose.set_focus_on_click(False) - sclose.set_relief(gtk.RELIEF_NONE) - sclose_icon = gtk.Image() - sclose_icon.set_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU) - sclose.add(sclose_icon) - sclose.set_name("terminator-search-close-button") - if hasattr(sclose, "set_tooltip_text"): - sclose.set_tooltip_text("Close Search Bar") - sclose.connect('clicked', self.end_search) - sclose.show_all() - - # Button for the next result. Explicitly not show()n by default. - self._search_next = gtk.Button(_("Next")) - self._search_next.connect('clicked', self.next_search) - - self._searchbox.pack_start(slabel, False) - self._search_result_label = gtk.Label() - self._search_result_label.set_text("") - self._search_result_label.show() - self._searchbox.pack_start(self._searchinput) - self._searchbox.pack_start(self._search_result_label, False) - self._searchbox.pack_start(self._search_next, False, False) - self._searchbox.pack_end(sclose, False, False) - - self.show() - self.pack_start(self._titlebox, False) - self.pack_start(self._termbox) - self.pack_end(self._searchbox) - - self._titlebox.update () - - self._scrollbar = gtk.VScrollbar (self._vte.get_adjustment ()) - if self.scrollbar_position != "hidden" and self.scrollbar_position != "disabled": - self._scrollbar.show () - - if self.scrollbar_position == 'left': - packfunc = self._termbox.pack_end - else: - packfunc = self._termbox.pack_start - - packfunc (self._vte) - packfunc (self._scrollbar, False) - - self._vte.connect ("key-press-event", self.on_vte_key_press) - self._vte.connect ("button-press-event", self.on_vte_button_press) - self._vte.connect ("popup-menu", self.create_popup_menu) - """drag and drop""" - srcvtetargets = [ ( "vte", gtk.TARGET_SAME_APP, self.TARGET_TYPE_VTE ) ] - dsttargets = [ ( "vte", gtk.TARGET_SAME_APP, self.TARGET_TYPE_VTE ), ('text/plain', 0, 0) , ("STRING", 0, 0), ("COMPOUND_TEXT", 0, 0)] - self._vte.drag_source_set( gtk.gdk.CONTROL_MASK | gtk.gdk.BUTTON3_MASK, srcvtetargets, gtk.gdk.ACTION_MOVE) - self._titlebox.drag_source_set( gtk.gdk.BUTTON1_MASK, srcvtetargets, gtk.gdk.ACTION_MOVE) - #self._vte.drag_dest_set(gtk.DEST_DEFAULT_MOTION | gtk.DEST_DEFAULT_HIGHLIGHT |gtk.DEST_DEFAULT_DROP ,dsttargets, gtk.gdk.ACTION_MOVE) - self._vte.drag_dest_set(gtk.DEST_DEFAULT_MOTION | gtk.DEST_DEFAULT_HIGHLIGHT |gtk.DEST_DEFAULT_DROP ,dsttargets, gtk.gdk.ACTION_MOVE) - self._vte.connect("drag-begin", self.on_drag_begin, self) - self._titlebox.connect("drag-begin", self.on_drag_begin, self) - self._vte.connect("drag-data-get", self.on_drag_data_get, self) - self._titlebox.connect("drag-data-get", self.on_drag_data_get, self) - #for testing purpose: drag-motion - self._vte.connect("drag-motion", self.on_drag_motion, self) - self._vte.connect("drag-data-received", self.on_drag_data_received, self) - - if self.conf.copy_on_selection: - self._vte.connect ("selection-changed", lambda widget: self._vte.copy_clipboard ()) - if self._composited_support : - self._vte.connect ("composited-changed", self.on_composited_changed) - self._vte.connect ("window-title-changed", self.on_vte_title_change) - self._vte.connect ("grab-focus", self.on_vte_focus) - self._vte.connect ("focus-out-event", self.on_vte_focus_out) - self._vte.connect ("focus-in-event", self.on_vte_focus_in) - self._vte.connect ("resize-window", self.on_resize_window) - self._vte.connect ("size-allocate", self.on_vte_size_allocate) - - self._titlebox.connect_icon (self.on_group_button_press) - - exit_action = self.conf.exit_action - if exit_action == "restart": - self._vte.connect ("child-exited", self.spawn_child) - # We need to support "left" because some buggy versions of gnome-terminal - # set it in some situations - elif exit_action in ("close", "left"): - self._vte.connect ("child-exited", lambda close_term: self.terminator.closeterm (self)) - - self._vte.add_events (gtk.gdk.ENTER_NOTIFY_MASK) - self._vte.connect ("enter_notify_event", self.on_vte_notify_enter) - - self._vte.connect_after ("realize", self.reconfigure_vte) - - self.add_matches(posix = self.conf.try_posix_regexp) - - env_proxy = os.getenv ('http_proxy') - if not env_proxy and self.conf.http_proxy and self.conf.http_proxy != '': - os.putenv ('http_proxy', self.conf.http_proxy) - - os.putenv ('COLORTERM', 'gnome-terminal') - - def prepareurl (self, url, match): - dbg ("prepareurl: Checking '%s' with a match of '%s'" % (url, match)) - if match == self.matches['email'] and url[0:7] != 'mailto:': - url = 'mailto:' + url - elif match == self.matches['addr_only'] and url[0:3] == 'ftp': - url = 'ftp://' + url - elif match == self.matches['addr_only']: - url = 'http://' + url - elif match == self.matches['launchpad-bug']: - for item in re.findall(r'[0-9]+',url): - url = 'https://bugs.launchpad.net/bugs/%s' % item - return url - elif match == self.matches['launchpad-branch']: - url = url[3:] if url.startswith('lp:') else url - url = 'https://code.launchpad.net/+branch/%s' % url - elif match == self.matches['apturl']: - # xdg-open will work as-is with apt: URLs - pass - - return url - - def openurl (self, url): - dbg ('openurl: viewing %s'%url) - try: - dbg ('openurl: calling xdg-open') - subprocess.Popen(["xdg-open", url]) - except: - dbg ('openurl: xdg-open failed') - try: - dbg ('openurl: calling url_show') - self.terminator.url_show (url) - except: - dbg ('openurl: url_show failed. No URL for you') - pass - - def on_resize_window(self, widget, width, height): - dbg ('Resize window triggered on %s: %dx%d' % (widget, width, height)) - - def on_vte_size_allocate(self, widget, allocation): - dbg ('Terminal resized to %dx%d' % (self._vte.get_column_count (), self._vte.get_row_count ())) - self._titlebox.set_terminal_size (self._vte.get_column_count (), self._vte.get_row_count ()) - if self._vte.window != None and (self.conf.geometry_hinting): - self.terminator.on_term_resized () - - def get_pixbuf(self, maxsize= None): - pixmap = self.get_snapshot() - (width, height) = pixmap.get_size() - pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, width, height) - pixbuf.get_from_drawable(pixmap, pixmap.get_colormap(), 0, 0, 0, 0, width, height) - - longest = max(width, height) - - if maxsize is not None: - factor = float(maxsize) / float(longest) - - if not maxsize or (width * factor) > width or (height * factor) > height: - factor = 1 - - scaledpixbuf = pixbuf.scale_simple (int(width * factor), int(height * factor), gtk.gdk.INTERP_BILINEAR) - - return(scaledpixbuf) - - def on_drag_begin(self, widget, drag_context, data): - dbg ('Drag begins') - widget.drag_source_set_icon_pixbuf(self.get_pixbuf (512)) - - def on_drag_data_get(self,widget, drag_context, selection_data, info, time, data): - dbg ("Drag data get") - selection_data.set("vte",info, str(data.terminator.term_list.index (self))) - - def on_expose_event(self, widget, event): - if widget._expose_data is None: - return False - - color = widget._expose_data['color'] - coord = widget._expose_data['coord'] - - context = widget.window.cairo_create() - #leaving those xxx_group as they could be usefull - ##http://macslow.thepimp.net/?p=153 - #context.push_group() - context.set_source_rgba(color.red, color.green, color.blue, 0.5) - if len(coord) > 0 : - context.move_to(coord[len(coord)-1][0],coord[len(coord)-1][1]) - for i in coord: - context.line_to(i[0],i[1]) - - context.fill() - #context.pop_group_to_source() - #context.paint() - return False - - def on_drag_motion(self, widget, drag_context, x, y, time, data): - dbg ("Drag Motion on ") - """ -x-special/gnome-icon-list -text/uri-list -UTF8_STRING -COMPOUND_TEXT -TEXT -STRING -text/plain;charset=utf-8 -text/plain;charset=UTF-8 -text/plain - """ - - if 'text/plain' in drag_context.targets: - #copy text from another widget - return - srcwidget = drag_context.get_source_widget() - if (isinstance(srcwidget, gtk.EventBox) and srcwidget == self._titlebox) or widget == srcwidget: - #on self - return - - alloc = widget.allocation - rect = gtk.gdk.Rectangle(0, 0, alloc.width, alloc.height) - - if self.conf.use_theme_colors: - color = self._vte.get_style ().text[gtk.STATE_NORMAL] - else: - color = gtk.gdk.color_parse (self.conf.foreground_color) - - pos = self.get_location(widget, x, y) - topleft = (0,0) - topright = (alloc.width,0) - topmiddle = (alloc.width/2,0) - bottomleft = (0, alloc.height) - bottomright = (alloc.width,alloc.height) - bottommiddle = (alloc.width/2, alloc.height) - middle = (alloc.width/2, alloc.height/2) - middleleft = (0, alloc.height/2) - middleright = (alloc.width, alloc.height/2) - #print "%f %f %d %d" %(coef1, coef2, b1,b2) - coord = () - if pos == "right": - coord = (topright, topmiddle, bottommiddle, bottomright) - if pos == "top": - coord = (topleft, topright, middleright , middleleft) - if pos == "left": - coord = (topleft, topmiddle, bottommiddle, bottomleft) - if pos == "bottom": - coord = (bottomleft, bottomright, middleright , middleleft) - - - #here, we define some widget internal values - widget._expose_data = { 'color': color, 'coord' : coord } - #redraw by forcing an event - connec = widget.connect_after('expose-event', self.on_expose_event) - widget.window.invalidate_rect(rect, True) - widget.window.process_updates(True) - #finaly reset the values - widget.disconnect(connec) - widget._expose_data = None - - def on_drag_drop(self, widget, drag_context, x, y, time): - parent = widget.get_parent() - dbg ('Drag drop on %s'%parent) - - def get_target_terms(self): - if self.terminator.groupsend == 2: - return self.terminator.term_list - elif self.terminator.groupsend == 1: - term_subset = [] - for term in self.terminator.term_list: - if term == self or (term._group != None and term._group == self._group): - term_subset.append(term) - return term_subset - else: - return [self] - - def on_drag_data_received(self, widget, drag_context, x, y, selection_data, info, time, data): - dbg ("Drag Data Received") - if selection_data.type == 'text/plain': - #copy text to destination - #print "%s %s" % (selection_data.type, selection_data.target) - txt = selection_data.data.strip() - if txt[0:7] == "file://": - txt = "'%s'" % urllib2.unquote(txt[7:]) - for term in self.get_target_terms(): - term._vte.feed_child(txt) - return - - widgetsrc = data.terminator.term_list[int(selection_data.data)] - srcvte = drag_context.get_source_widget() - #check if computation requireds - if (isinstance(srcvte, gtk.EventBox) and srcvte == self._titlebox) or srcvte == widget: - dbg (" on itself") - return - - srchbox = widgetsrc - dsthbox = widget.get_parent().get_parent() - - dstpaned = dsthbox.get_parent() - srcpaned = srchbox.get_parent() - if isinstance(dstpaned, gtk.Window) and isinstance(srcpaned, gtk.Window): - dbg (" Only one terminal") - return - pos = self.get_location(widget, x, y) - - data.terminator.remove(widgetsrc, True) - data.terminator.add(self, widgetsrc,pos) - return - - def get_location(self, vte, x, y): - pos = "" - #get the diagonales function for the receiving widget - coef1 = float(vte.allocation.height)/float(vte.allocation.width) - coef2 = -float(vte.allocation.height)/float(vte.allocation.width) - b1 = 0 - b2 = vte.allocation.height - #determine position in rectangle - """ - -------- - |\ /| - | \ / | - | \/ | - | /\ | - | / \ | - |/ \| - -------- - """ - if (x*coef1 + b1 > y ) and (x*coef2 + b2 < y ): - pos = "right" - if (x*coef1 + b1 > y ) and (x*coef2 + b2 > y ): - pos = "top" - if (x*coef1 + b1 < y ) and (x*coef2 + b2 > y ): - pos = "left" - if (x*coef1 + b1 < y ) and (x*coef2 + b2 < y ): - pos = "bottom" - return pos - - def add_matches (self, posix = True): - userchars = "-A-Za-z0-9" - passchars = "-A-Za-z0-9,?;.:/!%$^*&~\"#'" - hostchars = "-A-Za-z0-9" - pathchars = "-A-Za-z0-9_$.+!*(),;:@&=?/~#%'\"" - schemes = "(news:|telnet:|nntp:|file:/|https?:|ftps?:|webcal:)" - user = "[" + userchars + "]+(:[" + passchars + "]+)?" - urlpath = "/[" + pathchars + "]*[^]'.}>) \t\r\n,\\\"]" - - if posix: - dbg ('add_matches: Trying POSIX URL regexps. Set try_posix_regexp = False in config to only try GNU if you get (harmless) VTE warnings.') - lboundry = "[[:<:]]" - rboundry = "[[:>:]]" - else: # GNU - dbg ('add_matches: Trying GNU URL regexps. Set try_posix_regexp = True in config if URLs are not detected.') - lboundry = "\\<" - rboundry = "\\>" - - self.matches['full_uri'] = self._vte.match_add(lboundry + schemes + "//(" + user + "@)?[" + hostchars +".]+(:[0-9]+)?(" + urlpath + ")?" + rboundry + "/?") - - if self.matches['full_uri'] == -1: - if posix: - err ('add_matches: POSIX match failed, trying GNU') - self.add_matches(posix = False) - else: - err ('add_matches: Failed adding URL match patterns') - else: - self.matches['voip'] = self._vte.match_add(lboundry + '(callto:|h323:|sip:)' + "[" + userchars + "+][" + userchars + ".]*(:[0-9]+)?@?[" + pathchars + "]+" + rboundry) - self.matches['addr_only'] = self._vte.match_add (lboundry + "(www|ftp)[" + hostchars + "]*\.[" + hostchars + ".]+(:[0-9]+)?(" + urlpath + ")?" + rboundry + "/?") - self.matches['email'] = self._vte.match_add (lboundry + "(mailto:)?[a-zA-Z0-9][a-zA-Z0-9.+-]*@[a-zA-Z0-9][a-zA-Z0-9-]*\.[a-zA-Z0-9][a-zA-Z0-9-]+[.a-zA-Z0-9-]*" + rboundry) - self.matches['nntp'] = self._vte.match_add (lboundry + '''news:[-A-Z\^_a-z{|}~!"#$%&'()*+,./0-9;:=?`]+@[-A-Za-z0-9.]+(:[0-9]+)?''' + rboundry) - self.matches['apturl'] = self._vte.match_add ('\\bapt:/?/?\w+\\b') # apt+http isn't supported - # if the url looks like a Launchpad changelog closure entry LP: #92953 - make it a url to http://bugs.launchpad.net - # the regular expression is similar to the perl one specified in the Ubuntu Policy Manual - /lp:\s+\#\d+(?:,\s*\#\d+)*/i - self.matches['launchpad-bug'] = self._vte.match_add ('\\b(lp|LP):?\s?#?[0-9]+(,\s*#?[0-9]+)*\\b') - # same for Bazaar branches hosted on Launchpad - lpfilters = {} - lpfilters['project'] = '[a-z0-9]{1}[a-z0-9\.\-\+]+' - lpfilters['group'] = '~%s' % lpfilters['project'] - lpfilters['series'] = lpfilters['project'] - lpfilters['branch'] = '[a-zA-Z0-9]{1}[a-zA-Z0-9_+@.-]+' - self.matches['launchpad-branch'] = self._vte.match_add ('\\b((lp|LP):%(project)s(/%(series)s)?|(lp|LP):%(group)s/(%(project)s|\+junk)/%(branch)s)\\b' % lpfilters) - - def _path_lookup(self, command): - if os.path.isabs (command): - if os.path.isfile (command): - return command - else: - return None - elif command[:2] == './' and os.path.isfile(command): - dbg('path_lookup: Relative filename "%s" found in cwd' % command) - return command - - try: - paths = os.environ['PATH'].split(':') - if len(paths[0]) == 0: raise (ValueError) - except (ValueError, NameError): - dbg('path_lookup: PATH not set in environment, using fallbacks') - paths = ['/usr/local/bin', '/usr/bin', '/bin'] - - dbg('path_lookup: Using %d paths: %s' % (len(paths), paths)) - - for path in paths: - target = os.path.join (path, command) - if os.path.isfile (target): - dbg('path_lookup: found "%s"' % target) - return target - - dbg('path_lookup: Unable to locate "%s"' % command) - - def _shell_lookup(self): - shells = [os.getenv('SHELL'), pwd.getpwuid(os.getuid())[6], - 'bash', 'zsh', 'tcsh', 'ksh', 'csh', 'sh'] - - for shell in shells: - if shell is None: continue - elif os.path.isfile (shell): - return shell - else: - rshell = self._path_lookup(shell) - if rshell is not None: - dbg('shell_lookup: Found "%s" at "%s"' % (shell, rshell)) - return rshell - - dbg('shell_lookup: Unable to locate a shell') - - def spawn_child (self, event=None): - update_records = self.conf.update_records - login = self.conf.login_shell - args = [] - shell = None - command = None - - if self.command: - dbg ('spawn_child: using self.command: %s' % self.command) - command = self.command - elif self.conf.use_custom_command: - dbg ('spawn_child: using custom command: %s' % self.conf.custom_command) - command = self.conf.custom_command - - if type(command) is list: - # List of arguments from -x - dbg('spawn_child: Bypassing shell and trying to run "%s" directly' % command[0]) - shell = self._path_lookup(command[0]) - args = command - else: - shell = self._shell_lookup() - - if self.conf.login_shell: - args.insert(0, "-%s" % shell) - else: - args.insert(0, shell) - - if command is not None: - args += ['-c', command] - - if shell is None: - # Give up, we're completely stuck - err (_('Unable to find a shell')) - gobject.timeout_add (100, self.terminator.closeterm, self) - return (-1) - - os.putenv ('WINDOWID', '%s' % self._vte.get_parent_window().xid) - - self._pid = self._vte.fork_command (command = shell, argv = args, - envv = [], loglastlog = login, logwtmp = update_records, - logutmp = update_records, directory=self.cwd) - - self.on_vte_title_change(self._vte) # Force an initial update of our titles - self._titlebox.update () - - if self._pid == -1: - err (_('Unable to start shell: ') + shell) - return (-1) - - def get_cwd (self): - """ Return the current working directory of the subprocess. - This function requires OS specific behaviours - """ - try: - cwd = self.terminator.pid_get_cwd (self._pid) - except OSError: - err ('get_cwd: unable to get cwd of %d' % self._pid) - cwd = '~' - pass - dbg ('get_cwd found: %s'%cwd) - return (cwd) - - def reconfigure_vte (self, widget = None): - # Set our emulation - self._vte.set_emulation (self.conf.emulation) - - # Set our charset - if self._custom_encoding == False or self._custom_encoding == None: - self._vte.set_encoding (self.conf.encoding) - - # Set our wordchars - self._vte.set_word_chars (self.conf.word_chars) - - # Set our mouselation - self._vte.set_mouse_autohide (self.conf.mouse_autohide) - - # Set our compatibility - backspace = self.conf.backspace_binding - delete = self.conf.delete_binding - -# Note, each of the 4 following comments should replace the line beneath it, but the python-vte bindings don't appear to support this constant, so the magic values are being assumed from the C enum :/ - if backspace == "ascii-del": -# backbind = vte.ERASE_ASCII_BACKSPACE - backbind = 2 - else: -# backbind = vte.ERASE_AUTO_BACKSPACE - backbind = 1 - - if delete == "escape-sequence": -# delbind = vte.ERASE_DELETE_SEQUENCE - delbind = 3 - else: -# delbind = vte.ERASE_AUTO - delbind = 0 - - self._vte.set_backspace_binding (backbind) - self._vte.set_delete_binding (delbind) - - # Set our font - if not self._custom_font_size: - try: - self._vte.set_font (pango.FontDescription (self.conf.font)) - except: - pass - - # Set our boldness - self._vte.set_allow_bold (self.conf.allow_bold) - - # Set our color scheme - palette = self.conf.palette - if self.conf.use_theme_colors: - fg_color = self._vte.get_style ().text[gtk.STATE_NORMAL] - bg_color = self._vte.get_style ().base[gtk.STATE_NORMAL] - else: - fg_color = gtk.gdk.color_parse (self.conf.foreground_color) - bg_color = gtk.gdk.color_parse (self.conf.background_color) - - colors = palette.split (':') - palette = [] - for color in colors: - if color: - palette.append (gtk.gdk.color_parse (color)) - self._vte.set_colors (fg_color, bg_color, palette) - - cursor_color = self.conf.cursor_color - if cursor_color != '': - self._vte.set_color_cursor (gtk.gdk.color_parse (cursor_color)) - - # Set cursor shape - if hasattr (self._vte, "set_cursor_shape"): - self._vte.set_cursor_shape (getattr (vte, "CURSOR_SHAPE_" + self.conf.cursor_shape.upper ())) - - # Set our background image, transparency and type - # Many thanks to the authors of gnome-terminal, on which this code is based. - background_type = self.conf.background_type - dbg ('H9TRANS: Configuring background type as: %s' % background_type) - - # set background image settings - if background_type == "image" and self.conf.background_image is not None and self.conf.background_image != '': - dbg ('H9TRANS: Setting background image to: %s' % self.conf.background_image) - self._vte.set_background_image_file (self.conf.background_image) - dbg ('H9TRANS: Setting background image scroll to: %s' % self.conf.scroll_background) - self._vte.set_scroll_background (self.conf.scroll_background) - else: - dbg ('H9TRANS: Unsetting background image') - self._vte.set_background_image_file('') - dbg ('H9TRANS: Unsetting background image scrolling') - self._vte.set_scroll_background(False) - - # set transparency for the background (image) - opacity = 65535 - if background_type in ("image", "transparent"): - self._vte.set_background_tint_color (gtk.gdk.color_parse (self.conf.background_color)) - self._vte.set_background_saturation(1 - (self.conf.background_darkness)) - opacity = int(self.conf.background_darkness * 65535) - dbg ('H9TRANS: Set background tint color to: %s' % self.conf.background_color) - dbg ('H9TRANS: Set background saturation to: %s' % (1 - (self.conf.background_darkness))) - else: - dbg ('H9TRANS: Set background saturation to: 1') - self._vte.set_background_saturation(1) - - if self._composited_support: - dbg ('H9TRANS: Set opacity to: %s' % opacity) - self._vte.set_opacity(opacity) - - if background_type == "transparent": - if not self.conf.enable_real_transparency: - self._vte.set_background_transparent (True) - else: - self._vte.set_background_transparent (False) - - # Set our cursor blinkiness - self._vte.set_cursor_blinks (self.conf.cursor_blink) - - if self.conf.force_no_bell: - self._vte.set_audible_bell (False) - self._vte.set_visible_bell (False) - if self._urgent_bell_cnid: - self._vte.disconnect (self._urgent_bell_cnid) - self._urgent_bell_cnid = None - else: - # Set our audible belliness - self._vte.set_audible_bell (self.conf.audible_bell) - - # Set our visual flashiness - self._vte.set_visible_bell (self.conf.visible_bell) - - # Set our urgent belliness - if self.conf.urgent_bell: - try: - self._urgent_bell_cnid = self._vte.connect ("beep", self.terminator.on_beep) - except TypeError: - err ("beep signal not supported by your VTE, urgent handler not available") - elif self._urgent_bell_cnid: - self._vte.disconnect (self._urgent_bell_cnid) - self._urgent_bell_cnid = None - - # Set our scrolliness - self._vte.set_scrollback_lines (self.conf.scrollback_lines) - self._vte.set_scroll_on_keystroke (self.conf.scroll_on_keystroke) - self._vte.set_scroll_on_output (self.conf.scroll_on_output) - - if self.scrollbar_position != self.conf.scrollbar_position: - self.scrollbar_position = self.conf.scrollbar_position - - if self.scrollbar_position == 'hidden' or self.scrollbar_position == 'disabled': - self._scrollbar.hide () - else: - self._scrollbar.show () - if self.scrollbar_position == 'right': - self._termbox.reorder_child (self._vte, 0) - elif self.scrollbar_position == 'left': - self._termbox.reorder_child (self._scrollbar, 0) - - if hasattr (self._vte, "set_alternate_screen_scroll"): - self._vte.set_alternate_screen_scroll (self.conf.alternate_screen_scroll) - - # Set our sloppiness - self.focus = self.conf.focus - - # Sync our titlebar state - self._titlebox.update () - self._vte.queue_draw () - - def get_size_details(self): - font_width = self._vte.get_char_width () - font_height = self._vte.get_char_height () - columns = self._vte.get_column_count () - rows = self._vte.get_row_count () - - return (font_width, font_height, columns, rows) - - def on_composited_changed (self, widget): - self.reconfigure_vte () - - def on_vte_button_press (self, term, event): - # Left mouse button + Ctrl while over a link should open it - mask = gtk.gdk.CONTROL_MASK - if (event.state & mask) == mask: - if event.button == 1: - url = self._vte.match_check (int (event.x / self._vte.get_char_width ()), int (event.y / self._vte.get_char_height ())) - if url: - self.openurl (self.prepareurl (url[0], url[1])) - return False - - # Left mouse button should transfer focus to this vte widget - # we also need to give focus on the widget where the paste occured - if event.button in (1 ,2): - if event.button == 2: - self.paste_clipboard (True) - return True - self._vte.grab_focus () - return False - - # Right mouse button should display a context menu if ctrl not pressed - if event.button == 3 and event.state & gtk.gdk.CONTROL_MASK == 0: - self.create_popup_menu (self._vte, event) - return True - - def on_vte_notify_enter (self, term, event): - if (self.focus == "sloppy" or self.focus == "mouse"): - term.grab_focus () - return False - - def do_autocleangroups_toggle (self): - self.terminator.autocleangroups = not self.terminator.autocleangroups - if self.terminator.autocleangroups: - self.terminator.group_hoover() - - def do_scrollbar_toggle (self): - self.toggle_widget_visibility (self._scrollbar) - - def do_splittogroup_toggle (self): - self.terminator.splittogroup = not self.terminator.splittogroup - - def do_title_toggle (self): - self._titlebox.wanted = not self._titlebox.get_property ('visible') - self.toggle_widget_visibility (self._titlebox) - - def toggle_widget_visibility (self, widget): - if not isinstance (widget, gtk.Widget): - raise TypeError - - if widget.get_property ('visible'): - widget.hide () - else: - widget.show () - - def paste_clipboard(self, primary = False): - for term in self.get_target_terms(): - if primary: - term._vte.paste_primary () - else: - term._vte.paste_clipboard () - self._vte.grab_focus() - - def do_enumerate(self, pad=False): - if pad: - numstr='%0'+str(len(str(len(self.terminator.term_list))))+'d' - else: - numstr='%d' - for term in self.get_target_terms(): - idx=self.terminator.term_list.index(term) - term._vte.feed_child(numstr % (idx+1)) - - #keybindings for the individual splited terminals (affects only the - #the selected terminal) - UnhandledKeybindings = ('close_window', 'full_screen') - def on_vte_key_press (self, term, event): - if not event: - dbg ('on_vte_key_press: Called on %s with no event' % term) - return False - mapping = self.terminator.keybindings.lookup(event) - - if mapping == "hide_window": - return False - - if mapping and mapping not in self.UnhandledKeybindings: - dbg("on_vte_key_press: lookup found %r" % mapping) - # handle the case where user has re-bound copy to ctrl+ - # we only copy if there is a selection otherwise let it fall through to ^ - if (mapping == "copy" and event.state & gtk.gdk.CONTROL_MASK): - if self._vte.get_has_selection (): - getattr(self, "key_" + mapping)() - return True - else: - getattr(self, "key_" + mapping)() - return True - - if self.terminator.groupsend != 0 and self._vte.is_focus (): - if self._group and self.terminator.groupsend == 1: - self.terminator.group_emit (self, self._group, 'key-press-event', event) - if self.terminator.groupsend == 2: - self.terminator.all_emit (self, 'key-press-event', event) - return False - - # Key events - def key_zoom_in(self): - self.zoom (True) - - def key_zoom_out(self): - self.zoom (False) - - def key_copy(self): - self._vte.copy_clipboard () - - def key_paste(self): - self.paste_clipboard () - - def key_toggle_scrollbar(self): - self.do_scrollbar_toggle () - - def key_zoom_normal(self): - self.zoom_orig () - - def key_search(self): - self.start_search() - - # bindings that should be moved to Terminator as they all just call - # a function of Terminator. It would be cleaner if TerminatorTerm - # has absolutely no reference to Terminator. - # N (next) - P (previous) - O (horizontal) - E (vertical) - W (close) - def key_new_root_tab(self): - self.terminator.newtab (self, True) - - def key_go_next(self): - self.terminator.go_next (self) - - def key_go_prev(self): - self.terminator.go_prev (self) - - def key_go_up(self): - self.terminator.go_up (self) - - def key_go_down(self): - self.terminator.go_down (self) - - def key_go_left(self): - self.terminator.go_left (self) - - def key_go_right(self): - self.terminator.go_right (self) - - def key_split_horiz(self): - self.terminator.splitaxis (self, False) - - def key_split_vert(self): - self.terminator.splitaxis (self, True) - - def key_close_term(self): - self.terminator.closeterm (self) - - def key_new_tab(self): - self.terminator.newtab(self) - - def key_resize_up(self): - self.terminator.resizeterm (self, 'Up') - - def key_resize_down(self): - self.terminator.resizeterm (self, 'Down') - - def key_resize_left(self): - self.terminator.resizeterm (self, 'Left') - - def key_resize_right(self): - self.terminator.resizeterm (self, 'Right') - - def key_move_tab_right(self): - self.terminator.move_tab (self, 'right') - - def key_move_tab_left(self): - self.terminator.move_tab (self, 'left') - - def key_toggle_zoom(self): - self.terminator.toggle_zoom (self) - - def key_scaled_zoom(self): - self.terminator.toggle_zoom (self, True) - - def key_next_tab(self): - self.terminator.next_tab (self) - - def key_prev_tab(self): - self.terminator.previous_tab (self) - - def key_switch_to_tab_1(self): - self.terminator.switch_to_tab (self, 0) - - def key_switch_to_tab_2(self): - self.terminator.switch_to_tab (self, 1) - - def key_switch_to_tab_3(self): - self.terminator.switch_to_tab (self, 2) - - def key_switch_to_tab_4(self): - self.terminator.switch_to_tab (self, 3) - - def key_switch_to_tab_5(self): - self.terminator.switch_to_tab (self, 4) - - def key_switch_to_tab_6(self): - self.terminator.switch_to_tab (self, 5) - - def key_switch_to_tab_7(self): - self.terminator.switch_to_tab (self, 6) - - def key_switch_to_tab_8(self): - self.terminator.switch_to_tab (self, 7) - - def key_switch_to_tab_9(self): - self.terminator.switch_to_tab (self, 8) - - def key_switch_to_tab_10(self): - self.terminator.switch_to_tab (self, 9) - - def key_reset(self): - self._vte.reset (True, False) - - def key_reset_clear(self): - self._vte.reset (True, True) - - def key_group_all(self): - self.group_all(self) - - def key_ungroup_all(self): - self.ungroup_all(self) - - def key_group_tab(self): - self.group_tab(self) - - def key_ungroup_tab(self): - self.ungroup_tab(self) - - def key_new_window(self): - cmd = sys.argv[0] - - if not os.path.isabs(cmd): - # Command is not an absolute path. Figure out where we are - cmd = os.path.join (self.terminator.origcwd, sys.argv[0]) - if not os.path.isfile(cmd): - # we weren't started as ./terminator in a path. Give up - err('Unable to locate Terminator') - return False - - dbg("Spawning: %s" % cmd) - subprocess.Popen([cmd,]) - # End key events - - def zoom_orig (self): - self._custom_font_size = None - self._vte.set_font (pango.FontDescription (self.conf.font)) - - def zoom (self, zoom_in): - pangodesc = self._vte.get_font () - fontsize = pangodesc.get_size () - - if fontsize > pango.SCALE and not zoom_in: - fontsize -= pango.SCALE - elif zoom_in: - fontsize += pango.SCALE - - pangodesc.set_size (fontsize) - self._custom_font_size = fontsize - self._vte.set_font (pangodesc) - - def start_search(self): - self._searchbox.show() - self._searchinput.grab_focus() - - def search_keypress(self, widget, event): - key = gtk.gdk.keyval_name(event.keyval) - if key == 'Escape': - self.end_search() - - def end_search(self, widget = None): - self._search_row = 0 - self._search_string = None - self._search_result_label.set_text("") - self._searchbox.hide() - self._scrollbar.set_value(self._vte.get_cursor_position()[1]) - self._vte.grab_focus() - - def do_search(self, widget): - string = widget.get_text() - dbg("do_search: Looking for %r" % string) - if string == '': - return - - if string != self._search_string: - self._search_row = self._get_vte_buffer_range()[0] - self._search_string = string - - self._search_result_label.set_text("Searching scrollback") - self.next_search() - - # Called by get_text_range, once per character. Argh. - def _search_character(self, widget, col, row, junk): - return True - - def next_search(self, widget=None): - startrow,endrow = self._get_vte_buffer_range() - while True: - if self._search_row == endrow: - self._search_row = startrow - self._search_result_label.set_text("Finished Search") - self._search_next.hide() - return - buffer = self._vte.get_text_range(self._search_row, 0, self._search_row, -1, self._search_character) - - # dbg("Row %d buffer: %r" % (self._search_row, buffer)) - index = buffer.find(self._search_string) - if index != -1: - self._search_result_label.set_text("Found at row %d" % self._search_row) - self._scrollbar.set_value(self._search_row) - self._search_row += 1 - self._search_next.show() - return - self._search_row += 1 - - def _get_vte_buffer_range(self): - column, endrow = self._vte.get_cursor_position() - startrow = max(0, endrow - self.conf.scrollback_lines) - return(startrow, endrow) - - def get_geometry (self): - '''Returns Gdk.Window.get_position(), pixel-based cursor position, - and Gdk.Window.get_geometry()''' - reply = dict() - if not self._vte.window: - return reply - x, y = self._vte.window.get_origin () - reply.setdefault('origin_x',x) - reply.setdefault('origin_y',y) - - column, row = self._vte.get_cursor_position () - cursor_x = column * self._vte.get_char_width () - cursor_y = row * self._vte.get_char_height () - reply.setdefault('cursor_x', cursor_x) - reply.setdefault('cursor_y', cursor_y) - - geometry = self._vte.window.get_geometry() - reply.setdefault('offset_x', geometry[0]) - reply.setdefault('offset_y', geometry[1]) - reply.setdefault('span_x', geometry[2]) - reply.setdefault('span_y', geometry[3]) - reply.setdefault('depth', geometry[4]) - - return reply - - def create_popup_menu (self, widget, event = None): - menu = gtk.Menu () - url = None - address = None - - if event: - url = self._vte.match_check (int (event.x / self._vte.get_char_width ()), int (event.y / self._vte.get_char_height ())) - button = event.button - time = event.time - else: - button = 0 - time = 0 - - if url: - address = self.prepareurl (url[0], url[1]) - - if url[1] == self.matches['email']: - nameopen = _("_Send Mail To...") - namecopy = _("_Copy Email Address") - item = gtk.MenuItem (nameopen) - elif url[1] == self.matches['voip']: - nameopen = _("Ca_ll To...") - namecopy = _("_Copy Call Address") - item = gtk.MenuItem (nameopen) - else: - nameopen = _("_Open Link") - namecopy = _("_Copy Link Address") - iconopen = gtk.image_new_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU) - - item = gtk.ImageMenuItem (nameopen) - item.set_property('image', iconopen) - - item.connect ("activate", lambda menu_item: self.openurl (address)) - menu.append (item) - - item = gtk.MenuItem (namecopy) - item.connect ("activate", lambda menu_item: self.clipboard.set_text (url[0])) - menu.append (item) - - item = gtk.MenuItem () - menu.append (item) - - item = gtk.ImageMenuItem (gtk.STOCK_COPY) - item.connect ("activate", lambda menu_item: self._vte.copy_clipboard ()) - item.set_sensitive (self._vte.get_has_selection ()) - menu.append (item) - - item = gtk.ImageMenuItem (gtk.STOCK_PASTE) - item.connect ("activate", lambda menu_item: self.paste_clipboard ()) - menu.append (item) - - item = gtk.MenuItem () - menu.append (item) - - if not self.terminator._zoomed: - str_horiz = _("Split H_orizontally") - str_vert = _("Split V_ertically") - - item = gtk.ImageMenuItem (str_horiz) - item_image = gtk.Image () - item_image.set_from_icon_name (APP_NAME + '_horiz', gtk.ICON_SIZE_MENU) - item.set_image (item_image) - if hasattr(item, "set_always_show_image"): - item.set_always_show_image (True) - - item.connect ("activate", lambda menu_item: self.terminator.splitaxis (self, False)) - menu.append (item) - item = gtk.ImageMenuItem (str_vert) - item_image = gtk.Image () - item_image.set_from_icon_name (APP_NAME + '_vert', gtk.ICON_SIZE_MENU) - item.set_image (item_image) - if hasattr(item, "set_always_show_image"): - item.set_always_show_image (True) - - item.connect ("activate", lambda menu_item: self.terminator.splitaxis (self, True)) - menu.append (item) - - item = gtk.MenuItem (_("Open _Tab")) - item.connect ("activate", lambda menu_item: self.terminator.newtab (self)) - menu.append (item) - - if self.terminator.debugaddress: - item = gtk.MenuItem (_("Open _Debug Tab")) - item.connect ("activate", lambda menu_item: self.terminator.newtab (self, command = "telnet %s" % ' '.join([str(x) for x in self.terminator.debugaddress]))) - menu.append (item) - - - if self.conf.extreme_tabs: - item = gtk.MenuItem (_("Open Top Level Tab")) - item.connect ("activate", lambda menu_item: self.terminator.newtab (self, True)) - menu.append (item) - - item = gtk.MenuItem () - menu.append (item) - - item = gtk.ImageMenuItem (gtk.STOCK_CLOSE) - item.connect ("activate", lambda menu_item: self.terminator.closeterm (self)) - menu.append (item) - - item = gtk.MenuItem () - menu.append (item) - - if len (self.terminator.term_list) > 1: - if not self.terminator._zoomed: - item = gtk.MenuItem (_("_Zoom terminal")) - item.connect ("activate", lambda menu_item: self.terminator.toggle_zoom (self, True)) - menu.append (item) - - item = gtk.MenuItem (_("Ma_ximise terminal")) - item.connect ("activate", lambda menu_item: self.terminator.toggle_zoom (self)) - menu.append (item) - else: - if self.terminator._zoomed and not self.terminator._maximised: - item = gtk.MenuItem (_("_Unzoom terminal")) - item.connect ("activate", lambda menu_item: self.terminator.toggle_zoom (self, True)) - menu.append (item) - - if self.terminator._zoomed and self.terminator._maximised: - item = gtk.MenuItem (_("Unma_ximise terminal")) - item.connect ("activate", lambda menu_item: self.terminator.toggle_zoom (self)) - menu.append (item) - - item = gtk.MenuItem () - menu.append (item) - - item = gtk.CheckMenuItem (_("Show _scrollbar")) - item.set_active (self._scrollbar.get_property ('visible')) - item.connect ("toggled", lambda menu_item: self.do_scrollbar_toggle ()) - menu.append (item) - - item = gtk.CheckMenuItem (_("Show _titlebar")) - item.set_active (self._titlebox.get_property ('visible')) - item.connect ("toggled", lambda menu_item: self.do_title_toggle ()) - if self._group: - item.set_sensitive (False) - menu.append (item) - - item = gtk.MenuItem (_("Ed_it profile")) - item.connect ("activate", lambda menu_item: self.terminator.edit_profile (self)) - menu.append (item) - - self._do_encoding_items (menu) - - menu.show_all () - menu.popup (None, None, None, button, time) - - return True - - def create_popup_group_menu (self, widget, event = None): - menu = gtk.Menu () - url = None - - if event: - url = self._vte.match_check (int (event.x / self._vte.get_char_width ()), int (event.y / self._vte.get_char_height ())) - button = event.button - time = event.time - else: - button = 0 - time = 0 - - self.populate_grouping_menu (menu) - - menu.show_all () - if gtk.gtk_version > (2, 14, 0): - menu.popup (None, None, self.position_popup_group_menu, button, time, widget) - else: - menu.popup (None, None, None, button, time, widget) - - return True - - def populate_grouping_menu (self, widget): - groupitem = None - - item = gtk.MenuItem (_("Assign to group...")) - item.connect ("activate", self.create_group) - widget.append (item) - - if len (self.terminator.groupings) > 0: - groupitem = gtk.RadioMenuItem (groupitem, _("None")) - groupitem.set_active (self._group == None) - groupitem.connect ("activate", self.set_group, None) - widget.append (groupitem) - - for group in self.terminator.groupings: - item = gtk.RadioMenuItem (groupitem, group, False) - item.set_active (self._group == group) - item.connect ("toggled", self.set_group, group) - widget.append (item) - groupitem = item - - if self._group != None or len (self.terminator.groupings) > 0: - item = gtk.MenuItem () - widget.append (item) - - if self._group != None: - item = gtk.MenuItem (_("Remove group %s") % (self._group)) - item.connect ("activate", self.ungroup, self._group) - widget.append (item) - - if self.terminator.get_first_parent_widget (self, gtk.Notebook) is not None and \ - not isinstance (self.get_parent(), gtk.Notebook): - item = gtk.MenuItem (_("G_roup all in tab")) - item.connect ("activate", self.group_tab) - widget.append (item) - - if self.terminator.get_first_parent_widget(self, gtk.Notebook) is not None and \ - not isinstance(self.get_parent(), gtk.Notebook) and \ - len(self.terminator.groupings) > 0: - item = gtk.MenuItem(_("Ungr_oup all in tab")) - item.connect("activate", self.ungroup_tab) - widget.append(item) - - if len (self.terminator.groupings) > 0: - item = gtk.MenuItem (_("Remove all groups")) - item.connect ("activate", self.ungroup_all) - widget.append (item) - - if self._group != None: - item = gtk.MenuItem () - widget.append (item) - - item = gtk.ImageMenuItem (_("Close group %s") % (self._group)) - grp_close_img = gtk.Image() - grp_close_img.set_from_stock(gtk.STOCK_CLOSE, 1) - item.set_image (grp_close_img) - item.connect ("activate", lambda menu_item: self.terminator.closegroupedterms (self)) - widget.append (item) - - item = gtk.MenuItem () - widget.append (item) - - groupitem = None - - groupitem = gtk.RadioMenuItem (groupitem, _("Broadcast off")) - groupitem.set_active (self.terminator.groupsend == 0) - groupitem.connect ("activate", self.set_groupsend, 0) - widget.append (groupitem) - - groupitem = gtk.RadioMenuItem (groupitem, _("Broadcast to group")) - groupitem.set_active (self.terminator.groupsend == 1) - groupitem.connect ("activate", self.set_groupsend, 1) - widget.append (groupitem) - - groupitem = gtk.RadioMenuItem (groupitem, _("Broadcast to all")) - groupitem.set_active (self.terminator.groupsend == 2) - groupitem.connect ("activate", self.set_groupsend, 2) - widget.append (groupitem) - - item = gtk.MenuItem () - widget.append (item) - - item = gtk.CheckMenuItem (_("Split to this group")) - item.set_active (self.terminator.splittogroup) - item.connect ("toggled", lambda menu_item: self.do_splittogroup_toggle ()) - if self._group == None: - item.set_sensitive(False) - widget.append (item) - - item = gtk.CheckMenuItem (_("Autoclean groups")) - item.set_active (self.terminator.autocleangroups) - item.connect ("toggled", lambda menu_item: self.do_autocleangroups_toggle ()) - widget.append (item) - - item = gtk.MenuItem () - widget.append (item) - - item = gtk.MenuItem (_("Insert terminal number")) - item.connect ("activate", lambda menu_item: self.do_enumerate ()) - widget.append (item) - - item = gtk.MenuItem (_("Insert padded terminal number")) - item.connect ("activate", lambda menu_item: self.do_enumerate (pad=True)) - widget.append (item) - - def position_popup_group_menu(self, menu, widget): - screen_w = gtk.gdk.screen_width() - screen_h = gtk.gdk.screen_height() - - widget_win = widget.get_window() - widget_x, widget_y = widget_win.get_origin() - widget_w, widget_h = widget_win.get_size() - - menu_w, menu_h = menu.size_request() - - if widget_y + widget_h + menu_h > screen_h: - menu_y = max(widget_y - menu_h, 0) - else: - menu_y = widget_y + widget_h - - return (widget_x, menu_y, 1) - - def create_group (self, item): - self.groupingscope = 0 - grplist=self.terminator.groupings[:] - grplist.sort() - - win = gtk.Window () - vbox = gtk.VBox (False, 6) - vbox.set_border_width(5) - win.add (vbox) - - # Populate the "Assign..." Section - contentvbox = gtk.VBox (False, 6) - selframe = gtk.Frame() - selframe_label = gtk.Label() - selframe_label.set_markup(_("Assign...")) - selframe.set_shadow_type(gtk.SHADOW_NONE) - selframe.set_label_widget(selframe_label) - selframe_align = gtk.Alignment(0, 0, 1, 1) - selframe_align.set_padding(0, 0, 12, 0) - selframevbox = gtk.VBox () - selframehbox = gtk.HBox () - - # Populate the Combo with existing group names (None at the top) - sel_combo = gtk.combo_box_new_text() - sel_combo.append_text(_("Terminals with no group")) - for grp in grplist: - sel_combo.append_text(grp) - sel_combo.set_sensitive(False) - - # Here are the radio buttons - groupitem = None - - groupitem = gtk.RadioButton (groupitem, _("Terminal")) - groupitem.set_active (True) - groupitem.connect ("toggled", self.set_groupingscope, 0, sel_combo) - selframehbox.pack_start (groupitem, False) - - groupitem = gtk.RadioButton (groupitem, _("Group")) - groupitem.connect ("toggled", self.set_groupingscope, 1, sel_combo) - selframehbox.pack_start (groupitem, False) - - groupitem = gtk.RadioButton (groupitem, _("All")) - groupitem.connect ("toggled", self.set_groupingscope, 2, sel_combo) - selframehbox.pack_start (groupitem, False) - - selframevbox.pack_start(selframehbox, True, True) - selframevbox.pack_start(sel_combo, True, True) - selframe_align.add(selframevbox) - selframe.add(selframe_align) - contentvbox.pack_start(selframe) - - # Populate the "To..." Section - tgtframe = gtk.Frame() - tgtframe_label = gtk.Label() - tgtframe_label.set_markup(_("To...")) - tgtframe.set_shadow_type(gtk.SHADOW_NONE) - tgtframe.set_label_widget(tgtframe_label) - tgtframe_align = gtk.Alignment(0, 0, 1, 1) - tgtframe_align.set_padding(0, 0, 12, 0) - tgtframevbox = gtk.VBox () - - # Populate the Combo with existing group names (None not needed) - tgt_comboentry = gtk.combo_box_entry_new_text() - for grp in grplist: - tgt_comboentry.append_text(grp) - - tgtframevbox.pack_start(tgt_comboentry, True, True) - - tgtframe_align.add(tgtframevbox) - tgtframe.add(tgtframe_align) - contentvbox.pack_start(tgtframe) - - okbut = gtk.Button (stock=gtk.STOCK_OK) - canbut = gtk.Button (stock=gtk.STOCK_CANCEL) - hbuttonbox = gtk.HButtonBox() - hbuttonbox.set_layout(gtk.BUTTONBOX_END) - hbuttonbox.pack_start (canbut, True, True) - hbuttonbox.pack_start (okbut, True, True) - - vbox.pack_start (contentvbox, False, True) - vbox.pack_end (hbuttonbox, False, True) - - canbut.connect ("clicked", lambda kill: win.destroy()) - okbut.connect ("clicked", self.do_create_group, win, sel_combo, tgt_comboentry) - tgt_comboentry.child.connect ("activate", self.do_create_group, win, sel_combo, tgt_comboentry) - - tgt_comboentry.grab_focus() - - # Center it over the current terminal (not perfect?!?) - # This could be replaced by a less bothersome dialog, but then that would - # center over the window, not the terminal - try: - screen_w = gtk.gdk.screen_width() - screen_h = gtk.gdk.screen_height() - local_x, local_y = self.allocation.x, self.allocation.y - local_w, local_h = self.allocation.width, self.allocation.height - window_x, window_y = self.get_window().get_origin() - x = window_x + local_x - y = window_y + local_y - win.realize() - new_x = min(max(0, x+(local_w/2)-(win.allocation.width/2)), screen_w-win.allocation.width) - new_y = min(max(0, y+(local_h/2)-(win.allocation.height/2)), screen_h-win.allocation.height) - win.move(new_x, new_y) - except AttributeError: - # Old pygtk has no gtk.Widget.get_window() - pass - - win.show_all () - - def set_groupingscope(self, widget, scope=None, sel_combo=None): - if widget.get_active(): - self.groupingscope = scope - if self.groupingscope == 1: - sel_combo.set_sensitive(True) - else: - sel_combo.set_sensitive(False) - - def do_create_group (self, widget, window, src, tgt): - tgt_name = tgt.child.get_text() - try: - src_name = src.get_active_text() - src_id = src.get_active() - except: - src_name = None - - if tgt_name == "" or (self.groupingscope == 1 and src_name == None): - return False - - if tgt_name not in self.terminator.groupings: - self.terminator.groupings.append (tgt_name) - - if self.groupingscope == 2: - for term in self.terminator.term_list: - term.set_group (None, tgt_name) - elif self.groupingscope == 1: - for term in self.terminator.term_list: - if term._group == src_name or (src_id == 0 and term._group == None): - term.set_group (None, tgt_name) - else: - self.set_group (None, tgt_name) - - window.destroy () - - def add_group (self, groupname): - if not groupname in self.terminator.groupings: - self.terminator.groupings.append(groupname) - - def set_group (self, item, data): - if self._group == data: - # No action needed - return - else: - self._group = data - - self._titlebox.set_group_label (data) - self._titlebox.update () - - if not self._group: - # We were not previously in a group - self._titlebox.show () - self._group = data - else: - # We were previously in a group - self._group = data - if data is None: - # We have been removed from a group - if not self.conf.titlebars and not self._want_titlebar: - self._titlebox.hide () - self.terminator.group_hoover () - - def set_groupsend (self, item, data): - self.terminator.groupsend = data - - def ungroup (self, widget, data): - for term in self.terminator.term_list: - if term._group == data: - term.set_group (None, None) - self.terminator.group_hoover () - - def group_all (self, widget): - allname = _("All") - self.add_group(allname) - for term in self.terminator.term_list: - term.set_group (None, allname) - self.on_vte_focus_in(self._vte, None) - self.terminator.group_hoover () - - def ungroup_all (self, widget): - for term in self.terminator.term_list: - term.set_group (None, None) - self.on_vte_focus_in(self._vte, None) - self.terminator.group_hoover () - - def find_all_terms_in_tab (self, notebook, pagenum=-1): - if pagenum == -1: - pagenum = notebook.get_current_page() - notebookchild = notebook.get_nth_page(pagenum) - - terms = [] - - for term in self.terminator.term_list: - termparent = term.get_parent() - while not isinstance(termparent, gtk.Window): - if termparent == notebookchild: - terms.append(term) - termparent = termparent.get_parent() - - return terms - - def group_tab (self, widget): - groupname = "" - notebook = self.terminator.get_first_parent_widget(self, gtk.Notebook) - pagenum = notebook.get_current_page() - notebookchild = notebook.get_nth_page(pagenum) - terms = self.find_all_terms_in_tab(notebook) - - notebooktablabel = notebook.get_tab_label(notebookchild) - if notebooktablabel._label._custom is True: - groupname = notebooktablabel.get_title() - - if groupname == "": - tmppagenum = pagenum - while True: - groupname = "Tab %d" % (tmppagenum + 1) - if groupname not in self.terminator.groupings: - break - tmppagenum += 1 - - self.add_group(groupname) - for term in terms: - term.set_group(None, groupname) - self.on_vte_focus_in(self._vte, None) - self.terminator.group_hoover() - - def ungroup_tab (self, widget): - notebook = self.terminator.get_first_parent_widget(self, gtk.Notebook) - terms = self.find_all_terms_in_tab (notebook) - - for term in terms: - term.set_group (None, None) - self.on_vte_focus_in(self._vte, None) - self.terminator.group_hoover() - - def on_encoding_change (self, widget, encoding): - current = self._vte.get_encoding () - if current != encoding: - dbg ('Setting Encoding to: %s' % encoding) - if encoding == self.conf.encoding: - self._custom_encoding = False - else: - self._custom_encoding = True - self._vte.set_encoding (encoding) - - def _do_encoding_items (self, menu): - active_encodings = self.conf.active_encodings - item = gtk.MenuItem (_("Encodings")) - menu.append (item) - submenu = gtk.Menu () - item.set_submenu (submenu) - encodings = TerminatorEncoding ().get_list () - encodings.sort (lambda x, y: cmp (x[2].lower (), y[2].lower ())) - - current_encoding = self._vte.get_encoding () - group = None - - if current_encoding not in active_encodings: - active_encodings.insert (0, _(current_encoding)) - - for encoding in active_encodings: - if encoding == self._default_encoding: - extratext = " (%s)" % _("Default") - elif encoding == current_encoding and self._custom_encoding == True: - extratext = " (%s)" % _("User defined") - else: - extratext = "" - - radioitem = gtk.RadioMenuItem (group, _(encoding) + extratext) - - if encoding == current_encoding: - radioitem.set_active (True) - - if group is None: - group = radioitem - - radioitem.connect ('activate', self.on_encoding_change, encoding) - submenu.append (radioitem) - - item = gtk.MenuItem (_("Other Encodings")) - submenu.append (item) - #second level - - submenu = gtk.Menu () - item.set_submenu (submenu) - group = None - - for encoding in encodings: - if encoding[1] in active_encodings: - continue - - if encoding[1] is None: - label = "%s %s"%(encoding[2], self._vte.get_encoding ()) - else: - label = "%s %s"%(encoding[2], encoding[1]) - - radioitem = gtk.RadioMenuItem (group, label) - if group is None: - group = radioitem - - if encoding[1] == current_encoding: - radioitem.set_active (True) - - radioitem.connect ('activate', self.on_encoding_change, encoding[1]) - submenu.append (radioitem) - - def get_window_title(self, vte = None): - if vte is None: - vte = self._vte - title = vte.get_window_title () - if title is None: - title = str(self.command) - return title - - def on_vte_title_change(self, vte): - title = self.get_window_title(vte) - if title == self._oldtitle: - # Title hasn't changed, don't bother doing anything - return - self._oldtitle = title - - if self.conf.titletips: - vte.set_property ("has-tooltip", True) - vte.set_property ("tooltip-text", title) - #set the title anyhow, titlebars setting only show/hide the label - self._titlebox.set_terminal_title (title) - self.terminator.set_window_title (title) - notebookpage = self.terminator.get_first_notebook_page(vte) - while notebookpage != None: - if notebookpage[0].get_tab_label(notebookpage[1]): - label = notebookpage[0].get_tab_label(notebookpage[1]) - label.set_title(title) - # FIXME: Is this necessary? The above line should update the label. LP #369370 might be related - notebookpage[0].set_tab_label(notebookpage[1], label) - notebookpage = self.terminator.get_first_notebook_page(notebookpage[0]) - - def on_vte_focus_in(self, vte, event): - for term in self.terminator.term_list: - term._titlebox.update_colors(self) - return - - def on_vte_focus_out(self, vte, event): - return - - def on_vte_focus(self, vte): - title = self.get_window_title(vte) - self.terminator.set_window_title(title) - notebookpage = self.terminator.get_first_notebook_page(vte) - while notebookpage != None: - if notebookpage[0].get_tab_label(notebookpage[1]): - label = notebookpage[0].get_tab_label(notebookpage[1]) - label.set_title(title) - notebookpage[0].set_tab_label(notebookpage[1], label) - notebookpage = self.terminator.get_first_notebook_page(notebookpage[0]) - - def is_scrollbar_present(self): - return self._scrollbar.get_property('visible') - - def on_group_button_press(self, term, event): - if event.button == 1: - self.create_popup_group_menu(term, event) - return False diff --git a/terminatorlib/tests/__init__.py b/terminatorlib/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/terminatorlib/tests/test_doctests.py b/terminatorlib/tests/test_doctests.py new file mode 100644 index 00000000..cb63ece0 --- /dev/null +++ b/terminatorlib/tests/test_doctests.py @@ -0,0 +1,17 @@ +"""Load up the tests.""" +from unittest import TestSuite +from doctest import DocTestSuite, ELLIPSIS + +def test_suite(): + suite = TestSuite() + for name in ( + 'config', + 'plugin', + 'cwd', + 'factory', + 'util', + 'tests.testborg', + 'tests.testsignalman', + ): + suite.addTest(DocTestSuite('terminatorlib.' + name)) + return suite diff --git a/terminatorlib/tests/testborg.py b/terminatorlib/tests/testborg.py new file mode 100755 index 00000000..634f74da --- /dev/null +++ b/terminatorlib/tests/testborg.py @@ -0,0 +1,55 @@ +#!/usr/bin/python +# Terminator by Chris Jones +# GPL v2 only +"""testborg.py - We are the borg. Resistance is futile. + doctests for borg.py + +>>> obj1 = TestBorg() +>>> obj2 = TestBorg() +>>> obj1.attribute +0 +>>> obj2.attribute +0 +>>> obj1.attribute = 12345 +>>> obj1.attribute +12345 +>>> obj2.attribute +12345 +>>> obj2.attribute = 54321 +>>> obj1.attribute +54321 +>>> obj3 = TestBorg2() +>>> obj3.attribute +1 +>>> obj4 = TestBorg2() +>>> obj3.attribute = 98765 +>>> obj4.attribute +98765 +>>> + +""" + +from ..borg import Borg + +class TestBorg(Borg): + attribute = None + + def __init__(self): + Borg.__init__(self, self.__class__.__name__) + self.prepare_attributes() + + def prepare_attributes(self): + if not self.attribute: + self.attribute = 0 + +class TestBorg2(Borg): + attribute = None + + def __init__(self): + Borg.__init__(self, self.__class__.__name__) + self.prepare_attributes() + + def prepare_attributes(self): + if not self.attribute: + self.attribute = 1 + diff --git a/terminatorlib/tests/testsignalman.py b/terminatorlib/tests/testsignalman.py new file mode 100755 index 00000000..ff6450a2 --- /dev/null +++ b/terminatorlib/tests/testsignalman.py @@ -0,0 +1,56 @@ +#!/usr/bin/python +# Terminator by Chris Jones +# GPL v2 only +"""testsignalman.py - Test the signalman class + +>>> widget = TestWidget() +>>> signalman = Signalman() +>>> signalman.new(widget, 'test1', handler) +>>> signalman.cnxids[widget].keys() +['test1'] +>>> widget.signals.values() +['test1'] +>>> signalman.remove_widget(widget) +>>> signalman.cnxids.has_key(widget) +False +>>> widget.signals.values() +[] +>>> signalman.new(widget, 'test2', handler) +>>> signalman.new(widget, 'test3', handler) +>>> signalman.remove_signal(widget, 'test2') +>>> signalman.cnxids[widget].keys() +['test3'] +>>> widget.signals.values() +['test3'] +>>> signalman.remove_widget(widget) +>>> + +""" + +from ..signalman import Signalman + +class TestWidget(): + signals = None + count = None + + def __init__(self): + self.signals = {} + self.count = 0 + + def connect(self, signal, handler, *args): + self.count = self.count + 1 + self.signals[self.count] = signal + return(self.count) + + def disconnect(self, signalid): + del(self.signals[signalid]) + +def handler(): + print "I am a test handler" + +if __name__ == '__main__': + import sys + import doctest + (failed, attempted) = doctest.testmod() + print "%d/%d tests failed" % (failed, attempted) + sys.exit(failed) diff --git a/terminatorlib/titlebar.py b/terminatorlib/titlebar.py new file mode 100755 index 00000000..b1bab28f --- /dev/null +++ b/terminatorlib/titlebar.py @@ -0,0 +1,207 @@ +#!/usr/bin/python +# Terminator by Chris Jones +# GPL v2 only +"""titlebar.py - classes necessary to provide a terminal title bar""" + +import gtk +import gobject + +from version import APP_NAME +from util import dbg +from terminator import Terminator +from editablelabel import EditableLabel + +# pylint: disable-msg=R0904 +# pylint: disable-msg=W0613 +class Titlebar(gtk.EventBox): + """Class implementing the Titlebar widget""" + + terminator = None + terminal = None + config = None + oldtitle = None + termtext = None + sizetext = None + label = None + ebox = None + groupicon = None + grouplabel = None + groupentry = None + + __gsignals__ = { + 'clicked': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + 'edit-done': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + 'create-group': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, + (gobject.TYPE_STRING,)), + } + + def __init__(self, terminal): + """Class initialiser""" + gtk.EventBox.__init__(self) + self.__gobject_init__() + + self.terminator = Terminator() + self.terminal = terminal + self.config = self.terminal.config + + self.label = EditableLabel() + self.label.connect('edit-done', self.on_edit_done) + self.ebox = gtk.EventBox() + grouphbox = gtk.HBox() + self.grouplabel = gtk.Label() + self.groupicon = gtk.Image() + + self.groupentry = gtk.Entry() + self.groupentry.set_no_show_all(True) + self.groupentry.connect('focus-out-event', self.groupentry_cancel) + self.groupentry.connect('activate', self.groupentry_activate) + self.groupentry.connect('key-press-event', self.groupentry_keypress) + + groupsend_type = self.terminator.groupsend_type + if self.terminator.groupsend == groupsend_type['all']: + icon_name = 'all' + elif self.terminator.groupsend == groupsend_type['group']: + icon_name = 'group' + elif self.terminator.groupsend == groupsend_type['off']: + icon_name = 'off' + self.set_from_icon_name('_active_broadcast_%s' % icon_name, + gtk.ICON_SIZE_MENU) + + grouphbox.pack_start(self.groupicon, False, True, 2) + grouphbox.pack_start(self.grouplabel, False, True, 2) + grouphbox.pack_start(self.groupentry, False, True, 2) + + self.ebox.add(grouphbox) + self.ebox.show_all() + + hbox = gtk.HBox() + hbox.pack_start(self.ebox, False, True, 0) + hbox.pack_start(gtk.VSeparator(), False, True, 0) + hbox.pack_start(self.label, True, True) + + self.add(hbox) + self.show_all() + + self.connect('button-press-event', self.on_clicked) + + def connect_icon(self, func): + """Connect the supplied function to clicking on the group icon""" + self.ebox.connect('button-release-event', func) + + def update(self, other=None): + """Update our contents""" + self.label.set_text("%s %s" % (self.termtext, self.sizetext)) + + if other: + term = self.terminal + terminator = self.terminator + if term != other and term.group and term.group == other.group: + if terminator.groupsend == terminator.groupsend_type['off']: + title_fg = self.config['title_inactive_fg_color'] + title_bg = self.config['title_inactive_bg_color'] + icon = '_receive_off' + else: + title_fg = self.config['title_receive_fg_color'] + title_bg = self.config['title_receive_bg_color'] + icon = '_receive_on' + group_fg = self.config['title_receive_fg_color'] + group_bg = self.config['title_receive_bg_color'] + elif term != other and not term.group or term.group != other.group: + if terminator.groupsend == terminator.groupsend_type['all']: + title_fg = self.config['title_receive_fg_color'] + title_bg = self.config['title_receive_bg_color'] + icon = '_receive_on' + else: + title_fg = self.config['title_inactive_fg_color'] + title_bg = self.config['title_inactive_bg_color'] + icon = '_receive_off' + group_fg = self.config['title_inactive_fg_color'] + group_bg = self.config['title_inactive_bg_color'] + else: + title_fg = self.config['title_transmit_fg_color'] + title_bg = self.config['title_transmit_bg_color'] + if terminator.groupsend == terminator.groupsend_type['all']: + icon = '_active_broadcast_all' + elif terminator.groupsend == terminator.groupsend_type['group']: + icon = '_active_broadcast_group' + else: + icon = '_active_broadcast_off' + group_fg = self.config['title_transmit_fg_color'] + group_bg = self.config['title_transmit_bg_color'] + + self.label.modify_fg(gtk.STATE_NORMAL, + gtk.gdk.color_parse(title_fg)) + self.grouplabel.modify_fg(gtk.STATE_NORMAL, + gtk.gdk.color_parse(group_fg)) + self.modify_bg(gtk.STATE_NORMAL, + gtk.gdk.color_parse(title_bg)) + self.ebox.modify_bg(gtk.STATE_NORMAL, + gtk.gdk.color_parse(group_bg)) + self.set_from_icon_name(icon, gtk.ICON_SIZE_MENU) + + def set_from_icon_name(self, name, size = gtk.ICON_SIZE_MENU): + """Set an icon for the group label""" + if not name: + self.groupicon.hide() + return + + self.groupicon.set_from_icon_name(APP_NAME + name, size) + self.groupicon.show() + + def update_terminal_size(self, width, height): + """Update the displayed terminal size""" + self.sizetext = "%sx%s" % (width, height) + self.update() + + def set_terminal_title(self, widget, title): + """Update the terminal title""" + self.termtext = title + self.update() + # Return False so we don't interrupt any chains of signal handling + return False + + def set_group_label(self, name): + """Set the name of the group""" + if name: + self.grouplabel.set_text(name) + self.grouplabel.show() + else: + self.grouplabel.hide() + + def on_clicked(self, widget, event): + """Handle a click on the label""" + self.emit('clicked') + + def on_edit_done(self, widget): + """Re-emit an edit-done signal from an EditableLabel""" + self.emit('edit-done') + + def editing(self): + """Determine if we're currently editing a group name or title""" + return(self.groupentry.get_property('visible') or self.label.editing()) + + def create_group(self): + """Create a new group""" + self.groupentry.show() + self.groupentry.grab_focus() + + def groupentry_cancel(self, widget, event): + """Hide the group name entry""" + self.groupentry.set_text('') + self.groupentry.hide() + self.get_parent().grab_focus() + + def groupentry_activate(self, widget): + """Actually cause a group to be created""" + groupname = self.groupentry.get_text() + dbg('Titlebar::groupentry_activate: creating group: %s' % groupname) + self.groupentry_cancel(None, None) + self.emit('create-group', groupname) + + def groupentry_keypress(self, widget, event): + """Handle keypresses on the entry widget""" + key = gtk.gdk.keyval_name(event.keyval) + if key == 'Escape': + self.groupentry_cancel(None, None) + +gobject.type_register(Titlebar) diff --git a/terminatorlib/translation.py b/terminatorlib/translation.py index 77477539..ff8bafef 100644 --- a/terminatorlib/translation.py +++ b/terminatorlib/translation.py @@ -1,6 +1,6 @@ #!/usr/bin/python # Terminator - multiple gnome terminals in one window -# Copyright (C) 2006-2008 cmsj@tenshu.net +# Copyright (C) 2006-2010 cmsj@tenshu.net # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -17,16 +17,22 @@ """Terminator by Chris Jones """ -from terminatorlib.version import APP_NAME +from version import APP_NAME +from util import dbg +_ = None + +# pylint: disable-msg=W0702 try: import gettext - gettext.install (APP_NAME) -except ImportError: - print "Using fallback _()" - import __builtin__ - def dummytrans (text): - """A _ function for systems without gettext. Effectively a NOOP""" - return text - __builtin__.__dict__['_'] = dummytrans + gettext.textdomain(APP_NAME) + _ = gettext.gettext +except: + dbg("Using fallback _()") + + def dummytrans (text): + """A _ function for systems without gettext. Effectively a NOOP""" + return(text) + + _ = dummytrans diff --git a/terminatorlib/util.py b/terminatorlib/util.py new file mode 100755 index 00000000..bca77884 --- /dev/null +++ b/terminatorlib/util.py @@ -0,0 +1,228 @@ +#!/usr/bin/python +# Terminator.util - misc utility functions +# Copyright (C) 2006-2010 cmsj@tenshu.net +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 2 only. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +"""Terminator.util - misc utility functions + +>>> a = {'foo': 'bar', 'baz': 'bjonk'} +>>> b = {'foo': 'far', 'baz': 'bjonk'} +>>> dict_diff(a, b) +{'foo': 'far'} + +""" + +import sys +import gtk +import os +import pwd +import inspect + +# set this to true to enable debugging output +DEBUG = False +# set this to true to additionally list filenames in debugging +DEBUGFILES = False + +def dbg(log = ""): + """Print a message if debugging is enabled""" + if DEBUG: + stackitem = inspect.stack()[1] + parent_frame = stackitem[0] + method = parent_frame.f_code.co_name + names, varargs, keywords, local_vars = inspect.getargvalues(parent_frame) + try: + self_name = names[0] + classname = local_vars[self_name].__class__.__name__ + except IndexError: + classname = "noclass" + if DEBUGFILES: + line = stackitem[2] + filename = parent_frame.f_code.co_filename + extra = " (%s:%s)" % (filename, line) + else: + extra = "" + print >> sys.stderr, "%s::%s: %s%s" % (classname, method, log, extra) + +def err(log = ""): + """Print an error message""" + print >> sys.stderr, log + +def gerr(message = None): + """Display a graphical error. This should only be used for serious + errors as it will halt execution""" + + dialog = gtk.MessageDialog(None, gtk.DIALOG_MODAL, + gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, message) + dialog.run() + +def has_ancestor(widget, wtype): + """Walk up the family tree of widget to see if any ancestors are of type""" + while widget: + widget = widget.get_parent() + if isinstance(widget, wtype): + return(True) + return(False) + +def get_top_window(widget): + """Return the Window instance a widget belongs to""" + parent = widget.get_parent() + while parent: + widget = parent + parent = widget.get_parent() + return(widget) + +def path_lookup(command): + '''Find a command in our path''' + if os.path.isabs(command): + if os.path.isfile(command): + return(command) + else: + return(None) + elif command[:2] == './' and os.path.isfile(command): + dbg('path_lookup: Relative filename %s found in cwd' % command) + return(command) + + try: + paths = os.environ['PATH'].split(':') + if len(paths[0]) == 0: + raise(ValueError) + except (ValueError, NameError): + dbg('path_lookup: PATH not set in environment, using fallbacks') + paths = ['/usr/local/bin', '/usr/bin', '/bin'] + + dbg('path_lookup: Using %d paths: %s' % (len(paths), paths)) + + for path in paths: + target = os.path.join(path, command) + if os.path.isfile(target): + dbg('path_lookup: found %s' % target) + return(target) + + dbg('path_lookup: Unable to locate %s' % command) + +def shell_lookup(): + """Find an appropriate shell for the user""" + shells = [os.getenv('SHELL'), pwd.getpwuid(os.getuid())[6], 'bash', + 'zsh', 'tcsh', 'ksh', 'csh', 'sh'] + + for shell in shells: + if shell is None: + continue + elif os.path.isfile(shell): + return(shell) + else: + rshell = path_lookup(shell) + if rshell is not None: + dbg('shell_lookup: Found %s at %s' % (shell, rshell)) + return(rshell) + dbg('shell_lookup: Unable to locate a shell') + +def widget_pixbuf(widget, maxsize=None): + """Generate a pixbuf of a widget""" + pixmap = widget.get_snapshot() + (width, height) = pixmap.get_size() + pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, width, height) + pixbuf.get_from_drawable(pixmap, pixmap.get_colormap(), 0, 0, 0, 0, width, + height) + + longest = max(width, height) + + if maxsize is not None: + factor = float(maxsize) / float(longest) + + if not maxsize or (width * factor) > width or (height * factor) > height: + factor = 1 + + scaledpixbuf = pixbuf.scale_simple(int(width * factor), int(height * factor), gtk.gdk.INTERP_BILINEAR) + + return(scaledpixbuf) + +def get_config_dir(): + """Expand all the messy nonsense for finding where ~/.config/terminator + really is""" + try: + configdir = os.environ['XDG_CONFIG_HOME'] + except KeyError: + configdir = os.path.join(os.path.expanduser('~'), '.config') + + return(os.path.join(configdir, 'terminator')) + +def dict_diff(reference, working): + """Examine the values in the supplied working set and return a new dict + that only contains those values which are different from those in the + reference dictionary""" + + result = {} + + for key in reference: + if reference[key] != working[key]: + result[key] = working[key] + + return(result) + +# Helper functions for directional navigation +def get_edge(allocation, direction): + """Return the edge of the supplied allocation that we will care about for + directional navigation""" + if direction == 'left': + edge = allocation.x + elif direction == 'up': + edge = allocation.y + elif direction == 'right': + edge = allocation.x + allocation.width + elif direction == 'down': + edge = allocation.y + allocation.height + else: + raise ValueError('unknown direction %s' % direction) + + return(edge) + +def get_nav_possible(edge, allocation, direction): + """Check if the supplied allocation is in the right direction of the + supplied edge""" + if direction == 'left': + return((allocation.x + allocation.width) < edge) + elif direction == 'right': + return(allocation.x > edge) + elif direction == 'up': + return((allocation.y + allocation.height) < edge) + elif direction == 'down': + return(allocation.y > edge) + else: + raise ValueError('Unknown direction: %s' % direction) + +def get_nav_offset(edge, allocation, direction): + """Work out how far edge is from a particular point on the allocation + rectangle, in the given direction""" + if direction == 'left': + return(edge - (allocation.x + allocation.width)) + elif direction == 'right': + return(edge + allocation.x) + elif direction == 'up': + return(edge - (allocation.y - allocation.height)) + elif direction == 'down': + return(edge + allocation.y) + else: + raise ValueError('Unknown direction: %s' % direction) + +def get_nav_tiebreak(direction, cursor_x, cursor_y, rect): + """We have multiple candidate terminals. Pick the closest by cursor + position""" + if direction in ['left', 'right']: + return(cursor_y >= rect.y and cursor_y <= (rect.y + rect.height)) + elif direction in ['up', 'down']: + return(cursor_x >= rect.x and cursor_x <= (rect.x + rect.width)) + else: + raise ValueError('Unknown direction: %s' % direction) + diff --git a/terminatorlib/version.py b/terminatorlib/version.py index 5d62880f..a0e9e3a6 100644 --- a/terminatorlib/version.py +++ b/terminatorlib/version.py @@ -1,6 +1,6 @@ #!/usr/bin/python # TerminatorVersion - version number -# Copyright (C) 2008 cmsj@tenshu.net +# Copyright (C) 2010 cmsj@tenshu.net # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -21,4 +21,4 @@ TerminatorVersion supplies our version number. """ APP_NAME = 'terminator' -APP_VERSION = '0.14' +APP_VERSION = '0.90' diff --git a/terminatorlib/window.py b/terminatorlib/window.py new file mode 100755 index 00000000..412ef24f --- /dev/null +++ b/terminatorlib/window.py @@ -0,0 +1,495 @@ +#!/usr/bin/python +# Terminator by Chris Jones +# GPL v2 only +"""window.py - class for the main Terminator window""" + +import pygtk +pygtk.require('2.0') +import gobject +import gtk +import glib + +from util import dbg, err +from translation import _ +from version import APP_NAME +from container import Container +from factory import Factory +from terminator import Terminator + +try: + import deskbar.core.keybinder as bindkey +except ImportError: + err('Unable to find python bindings for deskbar, "hide_window" is not' \ + 'available.') + +# pylint: disable-msg=R0904 +class Window(Container, gtk.Window): + """Class implementing a top-level Terminator window""" + + terminator = None + title = None + isfullscreen = None + ismaximised = None + hidebound = None + hidefunc = None + + zoom_data = None + term_zoomed = gobject.property(type=bool, default=False) + + def __init__(self): + """Class initialiser""" + self.terminator = Terminator() + self.terminator.register_window(self) + + Container.__init__(self) + gtk.Window.__init__(self) + gobject.type_register(Window) + self.register_signals(Window) + + self.set_property('allow-shrink', True) + self.apply_icon() + + self.register_callbacks() + self.apply_config() + + self.title = WindowTitle(self) + self.title.update() + + options = self.config.options_get() + if options: + if options.forcedtitle is not None: + self.title.force_title(options.forcedtitle) + + if options.role is not None: + self.set_role(options.role) + + if options.geometry is not None: + if not self.parse_geometry(options.geometry): + err('Window::__init__: Unable to parse geometry: %s' % + options.geometry) + + def register_callbacks(self): + """Connect the GTK+ signals we care about""" + self.connect('key-press-event', self.on_key_press) + self.connect('delete_event', self.on_delete_event) + self.connect('destroy', self.on_destroy_event) + self.connect('window-state-event', self.on_window_state_changed) + + # Attempt to grab a global hotkey for hiding the window. + # If we fail, we'll never hide the window, iconifying instead. + try: + self.hidebound = bindkey.tomboy_keybinder_bind( + self.config['keybindings']['hide_window'], + self.on_hide_window) + except (KeyError, NameError): + pass + + if not self.hidebound: + dbg('Unable to bind hide_window key, another instance has it.') + self.hidefunc = self.iconify + else: + self.hidefunc = self.hide + + def apply_config(self): + """Apply various configuration options""" + options = self.config.options_get() + maximise = self.config['window_state'] == 'maximise' + fullscreen = self.config['window_state'] == 'fullscreen' + hidden = self.config['window_state'] == 'hidden' + borderless = self.config['borderless'] + + if options: + if options.maximise: + maximise = True + if options.fullscreen: + fullscreen = True + if options.hidden: + hidden = True + if options.borderless: + borderless = True + + self.set_fullscreen(fullscreen) + self.set_maximised(maximise) + self.set_borderless(borderless) + self.set_real_transparency() + if self.hidebound: + self.set_hidden(hidden) + else: + self.set_iconified(hidden) + + def apply_icon(self): + """Set the window icon""" + icon_theme = gtk.IconTheme() + + try: + icon = icon_theme.load_icon(APP_NAME, 48, 0) + except (NameError, glib.GError): + dbg('Unable to load 48px Terminator icon') + icon = self.render_icon(gtk.STOCK_DIALOG_INFO, gtk.ICON_SIZE_BUTTON) + + self.set_icon(icon) + + def on_key_press(self, window, event): + """Handle a keyboard event""" + maker = Factory() + + self.set_urgency_hint(False) + + mapping = self.terminator.keybindings.lookup(event) + + if mapping: + dbg('Window::on_key_press: looked up %r' % mapping) + if mapping == 'full_screen': + self.set_fullscreen(not self.isfullscreen) + elif mapping == 'close_window': + if not self.on_delete_event(window, + gtk.gdk.Event(gtk.gdk.DELETE)): + self.on_destroy_event(window, + gtk.gdk.Event(gtk.gdk.DESTROY)) + elif mapping == 'new_tab': + self.tab_new() + else: + return(False) + return(True) + + def tab_new(self): + """Make a new tab""" + maker = Factory() + if not maker.isinstance(self.get_child(), 'Notebook'): + notebook = maker.make('Notebook', window=self) + self.get_child().newtab() + + def on_delete_event(self, window, event, data=None): + """Handle a window close request""" + maker = Factory() + if maker.isinstance(self.get_child(), 'Terminal'): + dbg('Window::on_delete_event: Only one child, closing is fine') + return(False) + return(self.confirm_close(window, _('window'))) + + def confirm_close(self, window, type): + """Display a confirmation dialog when the user is closing multiple + terminals in one window""" + dialog = self.construct_confirm_close(window, type) + result = dialog.run() + dialog.destroy() + return(not (result == gtk.RESPONSE_ACCEPT)) + + def on_destroy_event(self, widget, data=None): + """Handle window descruction""" + self.terminator.deregister_window(self) + self.destroy() + del(self) + + def on_hide_window(self, data): + """Handle a request to hide/show the window""" + pass + + # pylint: disable-msg=W0613 + def on_window_state_changed(self, window, event): + """Handle the state of the window changing""" + self.isfullscreen = bool(event.new_window_state & + gtk.gdk.WINDOW_STATE_FULLSCREEN) + self.ismaximised = bool(event.new_window_state & + gtk.gdk.WINDOW_STATE_MAXIMIZED) + dbg('Window::on_window_state_changed: fullscreen=%s, maximised=%s' % + (self.isfullscreen, self.ismaximised)) + + return(False) + + def set_maximised(self, value): + """Set the maximised state of the window from the supplied value""" + if value == True: + self.maximize() + else: + self.unmaximize() + + def set_fullscreen(self, value): + """Set the fullscreen state of the window from the supplied value""" + if value == True: + self.fullscreen() + else: + self.unfullscreen() + + def set_borderless(self, value): + """Set the state of the window border from the supplied value""" + self.set_decorated (not value) + + def set_hidden(self, value): + """Set the visibility of the window from the supplied value""" + pass + + def set_iconified(self, value): + """Set the minimised state of the window from the value""" + pass + + def set_real_transparency(self, value=True): + """Enable RGBA if supported on the current screen""" + screen = self.get_screen() + if value: + colormap = screen.get_rgba_colormap() + else: + colormap = screen.get_rgb_colormap() + + if colormap: + self.set_colormap(colormap) + + def add(self, widget): + """Add a widget to the window by way of gtk.Window.add()""" + maker = Factory() + gtk.Window.add(self, widget) + if maker.isinstance(widget, 'Terminal'): + signals = {'close-term': self.closeterm, + 'title-change': self.title.set_title, + 'split-horiz': self.split_horiz, + 'split-vert': self.split_vert, + 'unzoom': self.unzoom} + + for signal in signals: + self.connect_child(widget, signal, signals[signal]) + + self.connect_child(widget, 'tab-change', self.tab_change) + self.connect_child(widget, 'group-all', self.group_all) + self.connect_child(widget, 'ungroup-all', self.ungroup_all) + self.connect_child(widget, 'group-tab', self.group_tab) + self.connect_child(widget, 'ungroup-tab', self.ungroup_tab) + + widget.grab_focus() + + def remove(self, widget): + """Remove our child widget by way of gtk.Window.remove()""" + gtk.Window.remove(self, widget) + self.disconnect_child(widget) + return(True) + + def split_axis(self, widget, vertical=True, sibling=None): + """Split the window""" + maker = Factory() + self.remove(widget) + + if vertical: + container = maker.make('VPaned') + else: + container = maker.make('HPaned') + + if not sibling: + sibling = maker.make('Terminal') + self.add(container) + container.show_all() + + for term in [widget, sibling]: + container.add(term) + container.show_all() + + sibling.spawn_child() + + def zoom(self, widget, font_scale=True): + """Zoom a terminal widget""" + children = self.get_children() + + if widget in children: + # This widget is a direct child of ours and we're a Window + # so zooming is a no-op + return + + self.zoom_data = widget.get_zoom_data() + self.zoom_data['widget'] = widget + self.zoom_data['old_child'] = children[0] + self.zoom_data['font_scale'] = font_scale + + self.remove(self.zoom_data['old_child']) + self.zoom_data['old_parent'].remove(widget) + self.add(widget) + self.set_property('term_zoomed', True) + + if font_scale: + widget.cnxids.new(widget, 'size-allocate', + widget.zoom_scale, self.zoom_data) + + widget.grab_focus() + + def unzoom(self, widget): + """Restore normal terminal layout""" + if not self.get_property('term_zoomed'): + # We're not zoomed anyway + dbg('Window::unzoom: not zoomed, no-op') + return + + widget = self.zoom_data['widget'] + if self.zoom_data['font_scale']: + widget.vte.set_font(self.zoom_data['old_font']) + + self.remove(widget) + self.add(self.zoom_data['old_child']) + self.zoom_data['old_parent'].add(widget) + widget.grab_focus() + self.zoom_data = None + self.set_property('term_zoomed', False) + + def get_visible_terminals(self): + """Walk down the widget tree to find all of the visible terminals. + Mostly using Container::get_visible_terminals()""" + maker = Factory() + child = self.get_child() + terminals = {} + + # If our child is a Notebook, reset to work from its visible child + if maker.isinstance(child, 'Notebook'): + pagenum = child.get_current_page() + child = child.get_nth_page(pagenum) + + if maker.isinstance(child, 'Container'): + terminals.update(child.get_visible_terminals()) + elif maker.isinstance(child, 'Terminal'): + terminals[child] = child.get_allocation() + else: + err('Unknown child type %s' % type(child)) + + return(terminals) + + def set_rough_geometry_hints(self): + """Walk all the terminals along the top and left edges to fake up how + many columns/rows we sort of have""" + terminals = self.get_visible_terminals() + column_sum = 0 + row_sum = 0 + + for terminal in terminals: + rect = terminal.get_allocation() + if rect.x == 0: + cols, rows = terminal.get_size() + row_sum = row_sum + rows + if rect.y == 0: + cols, rows = terminal.get_size() + column_sum = column_sum + cols + + # FIXME: I don't think we should just use whatever font size info is on + # the last terminal we inspected. Looking up the default profile font + # size and calculating its character sizes would be rather expensive + # though. + font_width, font_height = terminal.get_font_size() + total_font_width = font_width * column_sum + total_font_height = font_height * row_sum + + win_width, win_height = self.get_size() + extra_width = win_width - total_font_width + extra_height = win_height - total_font_height + + self.set_geometry_hints(self, -1, -1, -1, -1, extra_width, + extra_height, font_width, font_height, -1.0, -1.0) + + def tab_change(self, widget, num=None): + """Change to a specific tab""" + if num is None: + err('must specify a tab to change to') + + maker = Factory() + child = self.get_child() + + if not maker.isinstance(child, 'Notebook'): + dbg('child is not a notebook, nothing to change to') + return + + if num == -1: + # Go to the next tab + cur = child.get_current_page() + pages = child.get_n_pages() + if cur == pages - 1: + num = 0 + elif num == -2: + # Go to the previous tab + cur = child.get_current_page() + if cur > 0: + num = cur - 1 + else: + num = child.get_n_pages() - 1 + + child.set_current_page(num) + # Work around strange bug in gtk-2.12.11 and pygtk-2.12.1 + # Without it, the selection changes, but the displayed page doesn't + # change + child.set_current_page(child.get_current_page()) + + # FIXME: All of these (un)group_(all|tab) methods need refactoring work + def group_all(self, widget): + """Group all terminals""" + # FIXME: Why isn't this being done by Terminator() ? + group = _('All') + self.terminator.create_group(group) + for terminal in self.terminator.terminals: + terminal.set_group(None, group) + + def ungroup_all(self, widget): + """Ungroup all terminals""" + for terminal in self.terminator.terminals: + terminal.set_group(None, None) + + def group_tab(self, widget): + """Group all terminals in the current tab""" + maker = Factory() + notebook = self.get_child() + + if not maker.isinstance(notebook, 'Notebook'): + dbg('not in a notebook, refusing to group tab') + return + + pagenum = notebook.get_current_page() + while True: + group = _('Tab %d') % pagenum + if group not in self.terminator.groups: + break + pagenum += 1 + for terminal in self.get_visible_terminals(): + terminal.set_group(None, group) + + def ungroup_tab(self, widget): + """Ungroup all terminals in the current tab""" + maker = Factory() + notebook = self.get_child() + + if not maker.isinstance(notebook, 'Notebook'): + dbg('note in a notebook, refusing to ungroup tab') + return + + for terminal in self.get_visible_terminals(): + terminal.set_group(None, None) + +class WindowTitle(object): + """Class to handle the setting of the window title""" + + window = None + text = None + forced = None + + def __init__(self, window): + """Class initialiser""" + self.window = window + self.forced = False + + def set_title(self, widget, text): + """Set the title""" + if not self.forced: + self.text = text + self.update() + + def force_title(self, newtext): + """Force a specific title""" + if newtext: + self.set_title(None, newtext) + self.forced = True + else: + self.forced = False + + def update(self): + """Update the title automatically""" + title = None + + # FIXME: What the hell is this for?! + if self.forced: + title = self.text + else: + title = "%s" % self.text + + self.window.set_title(title) + +# vim: set expandtab ts=4 sw=4: