Land epic-refactor branch after 5.5 months of work

This commit is contained in:
Chris Jones 2010-01-21 13:10:03 +00:00
commit 103428d0e3
53 changed files with 12594 additions and 5084 deletions

View File

@ -2,3 +2,6 @@
terminatorlib/*.pyc
.project
.pydevproject
terminatorlib/meliae
_trial_temp
terminatorc

View File

@ -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:

2
README
View File

@ -1,4 +1,4 @@
Terminator 0.14
Terminator 0.90
by Chris Jones <cmsj@tenshu.net> and others.
The goal of this project is to produce a useful tool for arranging terminals.

6
TODO
View File

@ -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

6
debian/changelog vendored
View File

@ -1,3 +1,9 @@
terminator (0.90~alpha1) lucid; urgency=low
* New upstream pre-release
-- Chris Jones <cmsj@tenshu.net> Tue, 05 Jan 2010 09:56:27 +0000
terminator (0.14) karmic; urgency=low
* New upstream release

2
debian/control vendored
View File

@ -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

3
debian/copyright vendored
View File

@ -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:
<Copyright (C) 2006-2008 Chris Jones>
<Copyright (C) 2006-2010 Chris Jones and others>
License:

View File

@ -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

View File

@ -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

View File

@ -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
)

View File

@ -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 <cmsj@tenshu.net>"""
# 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

View File

@ -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 <cmsj@tenshu.net> 0.90-1
- Attempt to update for 0.90 pre-release.
Note that this specfile is untested.
* Thu Jan 15 2009 Chris Jones <cmsj@tenshu.net> 0.12-1
- Remove patch application since this isn't a fedora build.
Note that this specfile is untested.

View File

@ -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 <cmsj@tenshu.net>"""

55
terminatorlib/borg.py Executable file
View File

@ -0,0 +1,55 @@
#!/usr/bin/python
# Terminator by Chris Jones <cmsj@tenshu.net>
# 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')

File diff suppressed because it is too large Load Diff

View File

@ -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)

View File

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

192
terminatorlib/container.py Executable file
View File

@ -0,0 +1,192 @@
#!/usr/bin/python
# Terminator by Chris Jones <cmsj@tenshu.net>
# 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(_('<big><b>Close multiple terminals?</b></big>'))
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:

52
terminatorlib/cwd.py Executable file
View File

@ -0,0 +1,52 @@
#!/usr/bin/python
# Terminator by Chris Jones <cmsj@tenshu.net>
# 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:

View File

@ -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

View File

@ -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)

View File

@ -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)

97
terminatorlib/factory.py Executable file
View File

@ -0,0 +1,97 @@
#!/usr/bin/python
# Terminator by Chris Jones <cmsj@tenshu.net>
# 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']))

View File

@ -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)

360
terminatorlib/notebook.py Executable file
View File

@ -0,0 +1,360 @@
#!/usr/bin/python
# Terminator by Chris Jones <cmsj@tenshu.net>
# 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:

101
terminatorlib/optionparse.py Executable file
View File

@ -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)

195
terminatorlib/paned.py Executable file
View File

@ -0,0 +1,195 @@
#!/usr/bin/python
# Terminator by Chris Jones <cmsj@tenshu.net>
# 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:

143
terminatorlib/plugin.py Executable file
View File

@ -0,0 +1,143 @@
#!/usr/bin/python
# Terminator by Chris Jones <cmsj@tenshu.net>
# 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
<testplugin.TestPlugin object at 0x...>
>>> 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)

View File

@ -0,0 +1,434 @@
#!/usr/bin/python
# Terminator by Chris Jones <cmsj@tenshu.net>
# 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()

View File

@ -0,0 +1,21 @@
# Terminator by Chris Jones <cmsj@tenshu.net?
# GPL v2 only
"""terminal_menu.py - Default plugins for the terminal menu"""
import gtk
import terminatorlib.plugin as plugin
# Every plugin you want Terminator to load *must* be listed in 'available'
# This is commented out because we don't want any menu item plugins by default
#available = ['MyFirstMenuItem']
available = []
class MyFirstMenuItem(plugin.MenuItem):
"""Simple proof of concept"""
capabilities = ['terminal_menu']
def callback(self, menuitems, menu, terminal):
"""Add our menu items to the menu"""
item = gtk.MenuItem('Some Menu Text')
menuitems.append(item)

View File

@ -0,0 +1,11 @@
import terminatorlib.plugin as plugin
# available must contain a list of all the classes that you want exposed
available = ['TestPlugin']
class TestPlugin(plugin.Plugin):
capabilities = ['test']
def do_test(self):
return('TestPluginWin')

View File

@ -0,0 +1,53 @@
# Terminator by Chris Jones <cmsj@tenshu.net?
# GPL v2 only
"""url_handlers.py - Default plugins for URL handling"""
import re
import terminatorlib.plugin as plugin
# Every plugin you want Terminator to load *must* be listed in 'available'
available = ['LaunchpadBugURLHandler', 'LaunchpadCodeURLHandler', 'APTURLHandler']
class LaunchpadBugURLHandler(plugin.URLHandler):
"""Launchpad Bug URL handler. If the URL looks like a Launchpad changelog
closure entry... 'LP: #12345' then it should be transformed into a
Launchpad Bug URL"""
capabilities = ['url_handler']
handler_name = 'launchpad_bug'
match = '\\b(lp|LP):?\s?#?[0-9]+(,\s*#?[0-9]+)*\\b'
def callback(self, url):
"""Look for the number in the supplied string and return it as a URL"""
for item in re.findall(r'[0-9]+', url):
url = 'https://bugs.launchpad.net/bugs/%s' % item
return(url)
class LaunchpadCodeURLHandler(plugin.URLHandler):
"""Launchpad Code URL handler. If the URL looks like a Launchpad project or
branch entry then it should be transformed into a code.launchpad.net URL"""
capabilities = ['url_handler']
handler_name = 'launchpad_code'
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_+@.-]+'
match = '\\b((lp|LP):%(project)s(/%(series)s)?|(lp|LP):%(group)s/(%(project)s|\+junk)/%(branch)s)\\b' % lpfilters
def callback(self, url):
"""Look for the number in the supplied string and return it as a URL"""
if url.startswith('lp:'):
url = url[3:]
return('https://code.launchpad.net/+branch/%s' % url)
class APTURLHandler(plugin.URLHandler):
"""APT URL handler. If there is a URL that looks like an apturl, handle
it appropriately"""
capabilities = ['url_handler']
handler_name = 'apturl'
match = '\\bapt:.*\\b'
def callback(self, url):
"""Actually we don't need to do anything for this to work"""
return(url)

File diff suppressed because it is too large Load Diff

View File

@ -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 ("<i><b>NOTE:</b> These settings will not be saved. See:</i> <tt>man terminator_config</tt>")
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)

863
terminatorlib/prefseditor.py Executable file
View File

@ -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()

11
terminatorlib/pylint.sh Executable file
View File

@ -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

194
terminatorlib/searchbar.py Executable file
View File

@ -0,0 +1,194 @@
#!/usr/bin/python
# Terminator by Chris Jones <cmsj@tenshu.net>
# 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)

57
terminatorlib/signalman.py Executable file
View File

@ -0,0 +1,57 @@
#!/usr/bin/python
# Terminator by Chris Jones <cmsj@tenshu.net>
# 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)

1279
terminatorlib/terminal.py Executable file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,241 @@
#!/usr/bin/python
# Terminator by Chris Jones <cmsj@tenshu.net>
# 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)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

View File

@ -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

55
terminatorlib/tests/testborg.py Executable file
View File

@ -0,0 +1,55 @@
#!/usr/bin/python
# Terminator by Chris Jones <cmsj@tenshu.net>
# 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

View File

@ -0,0 +1,56 @@
#!/usr/bin/python
# Terminator by Chris Jones <cmsj@tenshu.net>
# 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)

207
terminatorlib/titlebar.py Executable file
View File

@ -0,0 +1,207 @@
#!/usr/bin/python
# Terminator by Chris Jones <cmsj@tenshu.net>
# 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)

View File

@ -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 <cmsj@tenshu.net>"""
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

228
terminatorlib/util.py Executable file
View File

@ -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)

View File

@ -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'

495
terminatorlib/window.py Executable file
View File

@ -0,0 +1,495 @@
#!/usr/bin/python
# Terminator by Chris Jones <cmsj@tenshu.net>
# 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: