terminator-1.91/ 0000775 0001750 0001750 00000000000 13055372500 014130 5 ustar steve steve 0000000 0000000 terminator-1.91/terminatorlib/ 0000775 0001750 0001750 00000000000 13055372500 017003 5 ustar steve steve 0000000 0000000 terminator-1.91/terminatorlib/layoutlauncher.glade 0000664 0001750 0001750 00000010625 13054612071 023043 0 ustar steve steve 0000000 0000000
terminator-1.91/terminatorlib/borg.py 0000775 0001750 0001750 00000003713 13054612071 020314 0 ustar steve steve 0000000 0000000 #!/usr/bin/env python2
# Terminator by Chris Jones
# GPL v2 only
"""borg.py - We are the borg. Resistance is futile.
http://code.activestate.com/recipes/66531/
ActiveState's policy appears to be that snippets
exist to encourage re-use, but I can not find any
specific licencing terms.
"""
from util import dbg
# pylint: disable-msg=R0903
# pylint: disable-msg=R0921
class Borg:
"""Definition of a class that can never be duplicated. Correct usage is
thus:
>>> from borg import Borg
>>> class foo(Borg):
... # All attributes on a borg class *must* = None
... attribute = None
... def __init__(self):
... Borg.__init__(self, self.__class__.__name__)
... def prepare_attributes(self):
... if not self.attribute:
... self.attribute = []
...
>>> bar = foo()
>>> bar.prepare_attributes()
The important thing to note is that all attributes of borg classes *must* be
declared as being None. If you attempt to use static class attributes you
will get unpredicted behaviour. Instead, prepare_attributes() must be called
which will then see the attributes in the shared state, and initialise them
if necessary."""
__shared_state = {}
def __init__(self, borgtype=None):
"""Class initialiser. Overwrite our class dictionary with the shared
state. This makes us identical to every other instance of this class
type."""
if borgtype is None:
raise TypeError('Borg::__init__: You must pass a borgtype')
if not self.__shared_state.has_key(borgtype):
dbg('Borg::__init__: Preparing borg state for %s' % borgtype)
self.__shared_state[borgtype] = {}
self.__dict__ = self.__shared_state[borgtype]
def prepare_attributes(self):
"""This should be used to prepare any attributes of the borg class."""
raise NotImplementedError('prepare_attributes')
terminator-1.91/terminatorlib/terminal.py 0000775 0001750 0001750 00000221333 13054612071 021176 0 ustar steve steve 0000000 0000000 #!/usr/bin/env python2
# Terminator by Chris Jones
# GPL v2 only
"""terminal.py - classes necessary to provide Terminal widgets"""
from __future__ import division
import os
import signal
import gi
from gi.repository import GLib, GObject, Pango, Gtk, Gdk
gi.require_version('Vte', '2.91') # vte-0.38 (gnome-3.14)
from gi.repository import Vte
import subprocess
import urllib
from util import dbg, err, spawn_new_terminator, make_uuid, manual_lookup, display_manager
import util
from config import Config
from cwd import get_default_cwd
from factory import Factory
from terminator import Terminator
from titlebar import Titlebar
from terminal_popup_menu import TerminalPopupMenu
from searchbar import Searchbar
from translation import _
from signalman import Signalman
import plugin
from terminatorlib.layoutlauncher import LayoutLauncher
# pylint: disable-msg=R0904
class Terminal(Gtk.VBox):
"""Class implementing the VTE widget and its wrappings"""
__gsignals__ = {
'close-term': (GObject.SignalFlags.RUN_LAST, None, ()),
'title-change': (GObject.SignalFlags.RUN_LAST, None,
(GObject.TYPE_STRING,)),
'enumerate': (GObject.SignalFlags.RUN_LAST, None,
(GObject.TYPE_INT,)),
'group-tab': (GObject.SignalFlags.RUN_LAST, None, ()),
'group-tab-toggle': (GObject.SignalFlags.RUN_LAST, None, ()),
'ungroup-tab': (GObject.SignalFlags.RUN_LAST, None, ()),
'ungroup-all': (GObject.SignalFlags.RUN_LAST, None, ()),
'split-horiz': (GObject.SignalFlags.RUN_LAST, None,
(GObject.TYPE_STRING,)),
'split-vert': (GObject.SignalFlags.RUN_LAST, None,
(GObject.TYPE_STRING,)),
'rotate-cw': (GObject.SignalFlags.RUN_LAST, None, ()),
'rotate-ccw': (GObject.SignalFlags.RUN_LAST, None, ()),
'tab-new': (GObject.SignalFlags.RUN_LAST, None,
(GObject.TYPE_BOOLEAN, GObject.TYPE_OBJECT)),
'tab-top-new': (GObject.SignalFlags.RUN_LAST, None, ()),
'focus-in': (GObject.SignalFlags.RUN_LAST, None, ()),
'focus-out': (GObject.SignalFlags.RUN_LAST, None, ()),
'zoom': (GObject.SignalFlags.RUN_LAST, None, ()),
'maximise': (GObject.SignalFlags.RUN_LAST, None, ()),
'unzoom': (GObject.SignalFlags.RUN_LAST, None, ()),
'resize-term': (GObject.SignalFlags.RUN_LAST, None,
(GObject.TYPE_STRING,)),
'navigate': (GObject.SignalFlags.RUN_LAST, None,
(GObject.TYPE_STRING,)),
'tab-change': (GObject.SignalFlags.RUN_LAST, None,
(GObject.TYPE_INT,)),
'group-all': (GObject.SignalFlags.RUN_LAST, None, ()),
'group-all-toggle': (GObject.SignalFlags.RUN_LAST, None, ()),
'move-tab': (GObject.SignalFlags.RUN_LAST, None,
(GObject.TYPE_STRING,)),
}
TARGET_TYPE_VTE = 8
TARGET_TYPE_MOZ = 9
MOUSEBUTTON_LEFT = 1
MOUSEBUTTON_MIDDLE = 2
MOUSEBUTTON_RIGHT = 3
terminator = None
vte = None
terminalbox = None
scrollbar = None
titlebar = None
searchbar = None
group = None
cwd = None
origcwd = None
command = None
clipboard = None
pid = None
matches = None
regex_flags = None
config = None
default_encoding = None
custom_encoding = None
custom_font_size = None
layout_command = None
directory = None
fgcolor_active = None
fgcolor_inactive = None
bgcolor = None
palette_active = None
palette_inactive = None
composite_support = None
cnxids = None
targets_for_new_group = None
def __init__(self):
"""Class initialiser"""
GObject.GObject.__init__(self)
self.terminator = Terminator()
self.terminator.register_terminal(self)
# FIXME: Surely these should happen in Terminator::register_terminal()?
self.connect('enumerate', self.terminator.do_enumerate)
self.connect('focus-in', self.terminator.focus_changed)
self.connect('focus-out', self.terminator.focus_left)
self.matches = {}
self.cnxids = Signalman()
self.config = Config()
self.cwd = get_default_cwd()
self.origcwd = self.terminator.origcwd
self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
self.pending_on_vte_size_allocate = False
self.vte = Vte.Terminal()
self.vte._draw_data = None
if not hasattr(self.vte, "set_opacity") or \
not hasattr(self.vte, "is_composited"):
self.composite_support = False
else:
self.composite_support = True
dbg('composite_support: %s' % self.composite_support)
self.vte.show()
self.default_encoding = self.vte.get_encoding()
self.regex_flags = (GLib.RegexCompileFlags.OPTIMIZE | \
GLib.RegexCompileFlags.MULTILINE)
self.update_url_matches()
self.terminalbox = self.create_terminalbox()
self.titlebar = Titlebar(self)
self.titlebar.connect_icon(self.on_group_button_press)
self.titlebar.connect('edit-done', self.on_edit_done)
self.connect('title-change', self.titlebar.set_terminal_title)
self.titlebar.connect('create-group', self.really_create_group)
self.titlebar.show_all()
self.searchbar = Searchbar()
self.searchbar.connect('end-search', self.on_search_done)
self.show()
self.pack_start(self.titlebar, False, True, 0)
self.pack_start(self.terminalbox, True, True, 0)
self.pack_end(self.searchbar, True, True, 0)
self.connect_signals()
os.putenv('TERM', self.config['term'])
os.putenv('COLORTERM', self.config['colorterm'])
env_proxy = os.getenv('http_proxy')
if not env_proxy:
if self.config['http_proxy'] and self.config['http_proxy'] != '':
os.putenv('http_proxy', self.config['http_proxy'])
self.reconfigure()
self.vte.set_size(80, 24)
def get_vte(self):
"""This simply returns the vte widget we are using"""
return(self.vte)
def force_set_profile(self, widget, profile):
"""Forcibly set our profile"""
self.set_profile(widget, profile, True)
def set_profile(self, _widget, profile, force=False):
"""Set our profile"""
if profile != self.config.get_profile():
self.config.set_profile(profile, force)
self.reconfigure()
def get_profile(self):
"""Return our profile name"""
return(self.config.profile)
def switch_to_next_profile(self):
profilelist = self.config.list_profiles()
list_length = len(profilelist)
if list_length > 1:
if profilelist.index(self.get_profile()) + 1 == list_length:
self.force_set_profile(False, profilelist[0])
else:
self.force_set_profile(False, profilelist[profilelist.index(self.get_profile()) + 1])
def switch_to_previous_profile(self):
profilelist = self.config.list_profiles()
list_length = len(profilelist)
if list_length > 1:
if profilelist.index(self.get_profile()) == 0:
self.force_set_profile(False, profilelist[list_length - 1])
else:
self.force_set_profile(False, profilelist[profilelist.index(self.get_profile()) - 1])
def get_cwd(self):
"""Return our cwd"""
vte_cwd = self.vte.get_current_directory_uri()
if vte_cwd:
# OSC7 pwd gives an answer
return(GLib.filename_from_uri(vte_cwd)[0])
else:
# Fall back to old gtk2 method
return(self.terminator.pid_cwd(self.pid))
def close(self):
"""Close ourselves"""
dbg('close: called')
self.cnxids.remove_widget(self.vte)
self.emit('close-term')
try:
dbg('close: killing %d' % self.pid)
os.kill(self.pid, signal.SIGHUP)
except Exception, ex:
# We really don't want to care if this failed. Deep OS voodoo is
# not what we should be doing.
dbg('os.kill failed: %s' % ex)
pass
if self.vte:
self.terminalbox.remove(self.vte)
del(self.vte)
def create_terminalbox(self):
"""Create a GtkHBox containing the terminal and a scrollbar"""
terminalbox = Gtk.HBox()
self.scrollbar = Gtk.VScrollbar(self.vte.get_vadjustment())
self.scrollbar.set_no_show_all(True)
terminalbox.pack_start(self.vte, True, True, 0)
terminalbox.pack_start(self.scrollbar, False, True, 0)
terminalbox.show_all()
return(terminalbox)
def update_url_matches(self):
"""Update the regexps used to match URLs"""
userchars = "-A-Za-z0-9"
passchars = "-A-Za-z0-9,?;.:/!%$^*&~\"#'"
hostchars = "-A-Za-z0-9:\[\]"
pathchars = "-A-Za-z0-9_$.+!*(),;:@&=?/~#%'"
schemes = "(news:|telnet:|nntp:|file:/|https?:|ftps?:|webcal:)"
user = "[" + userchars + "]+(:[" + passchars + "]+)?"
urlpath = "/[" + pathchars + "]*[^]'.}>) \t\r\n,\\\"]"
lboundry = "\\b"
rboundry = "\\b"
re = (lboundry + schemes +
"//(" + user + "@)?[" + hostchars +".]+(:[0-9]+)?(" +
urlpath + ")?" + rboundry + "/?")
reg = GLib.Regex.new(re, self.regex_flags, 0)
self.matches['full_uri'] = self.vte.match_add_gregex(reg, 0)
if self.matches['full_uri'] == -1:
err ('Terminal::update_url_matches: Failed adding URL matches')
else:
re = (lboundry +
'(callto:|h323:|sip:)' + "[" + userchars + "+][" +
userchars + ".]*(:[0-9]+)?@?[" + pathchars + "]+" +
rboundry)
reg = GLib.Regex.new(re, self.regex_flags, 0)
self.matches['voip'] = self.vte.match_add_gregex(reg, 0)
re = (lboundry +
"(www|ftp)[" + hostchars + "]*\.[" + hostchars +
".]+(:[0-9]+)?(" + urlpath + ")?" + rboundry + "/?")
reg = GLib.Regex.new(re, self.regex_flags, 0)
self.matches['addr_only'] = self.vte.match_add_gregex(reg, 0)
re = (lboundry +
"(mailto:)?[a-zA-Z0-9][a-zA-Z0-9.+-]*@[a-zA-Z0-9]" +
"[a-zA-Z0-9-]*\.[a-zA-Z0-9][a-zA-Z0-9-]+" +
"[.a-zA-Z0-9-]*" + rboundry)
reg = GLib.Regex.new(re, self.regex_flags, 0)
self.matches['email'] = self.vte.match_add_gregex(reg, 0)
re = (lboundry +
"""news:[-A-Z\^_a-z{|}~!"#$%&'()*+,./0-9;:=?`]+@""" +
"[-A-Za-z0-9.]+(:[0-9]+)?" + rboundry)
reg = GLib.Regex.new(re, self.regex_flags, 0)
self.matches['nntp'] = self.vte.match_add_gregex(reg, 0)
# Now add any matches from plugins
try:
registry = plugin.PluginRegistry()
registry.load_plugins()
plugins = registry.get_plugins_by_capability('url_handler')
for urlplugin in plugins:
name = urlplugin.handler_name
match = urlplugin.match
if name in self.matches:
dbg('refusing to add duplicate match %s' % name)
continue
reg = GLib.Regex.new(match, self.regex_flags, 0)
self.matches[name] = self.vte.match_add_gregex(reg, 0)
dbg('added plugin URL handler for %s (%s) as %d' %
(name, urlplugin.__class__.__name__,
self.matches[name]))
except Exception, ex:
err('Exception occurred adding plugin URL match: %s' % ex)
def match_add(self, name, match):
"""Register a URL match"""
if name in self.matches:
err('Terminal::match_add: Refusing to create duplicate match %s' % name)
return
reg = GLib.Regex.new(match, self.regex_flags, 0)
self.matches[name] = self.vte.match_add_gregex(reg, 0)
def match_remove(self, name):
"""Remove a previously registered URL match"""
if name not in self.matches:
err('Terminal::match_remove: Unable to remove non-existent match %s' % name)
return
self.vte.match_remove(self.matches[name])
del(self.matches[name])
def maybe_copy_clipboard(self):
if self.config['copy_on_selection'] and self.vte.get_has_selection():
self.vte.copy_clipboard()
def connect_signals(self):
"""Connect all the gtk signals and drag-n-drop mechanics"""
self.scrollbar.connect('button-press-event', self.on_buttonpress)
self.cnxids.new(self.vte, 'key-press-event', self.on_keypress)
self.cnxids.new(self.vte, 'button-press-event', self.on_buttonpress)
self.cnxids.new(self.vte, 'scroll-event', self.on_mousewheel)
self.cnxids.new(self.vte, 'popup-menu', self.popup_menu)
srcvtetargets = [("vte", Gtk.TargetFlags.SAME_APP, self.TARGET_TYPE_VTE)]
dsttargets = [("vte", Gtk.TargetFlags.SAME_APP, self.TARGET_TYPE_VTE),
('text/x-moz-url', 0, self.TARGET_TYPE_MOZ),
('_NETSCAPE_URL', 0, 0)]
'''
The following should work, but on my system it corrupts the returned
TargetEntry's in the newdstargets with binary crap, causing "Segmentation
fault (core dumped)" when the later drag_dest_set gets called.
dsttargetlist = Gtk.TargetList.new([])
dsttargetlist.add_text_targets(0)
dsttargetlist.add_uri_targets(0)
dsttargetlist.add_table(dsttargets)
newdsttargets = Gtk.target_table_new_from_list(dsttargetlist)
'''
# FIXME: Temporary workaround for the problems with the correct way of doing things
dsttargets.extend([('text/plain', 0, 0),
('text/plain;charset=utf-8', 0, 0),
('TEXT', 0, 0),
('STRING', 0, 0),
('UTF8_STRING', 0, 0),
('COMPOUND_TEXT', 0, 0),
('text/uri-list', 0, 0)])
# Convert to target entries
srcvtetargets = [Gtk.TargetEntry.new(*tgt) for tgt in srcvtetargets]
dsttargets = [Gtk.TargetEntry.new(*tgt) for tgt in dsttargets]
dbg('Finalised drag targets: %s' % dsttargets)
for (widget, mask) in [
(self.vte, Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.BUTTON3_MASK),
(self.titlebar, Gdk.ModifierType.BUTTON1_MASK)]:
widget.drag_source_set(mask, srcvtetargets, Gdk.DragAction.MOVE)
self.vte.drag_dest_set(Gtk.DestDefaults.MOTION |
Gtk.DestDefaults.HIGHLIGHT | Gtk.DestDefaults.DROP,
dsttargets, Gdk.DragAction.COPY | Gdk.DragAction.MOVE)
for widget in [self.vte, self.titlebar]:
self.cnxids.new(widget, 'drag-begin', self.on_drag_begin, self)
self.cnxids.new(widget, 'drag-data-get', self.on_drag_data_get,
self)
self.cnxids.new(self.vte, 'drag-motion', self.on_drag_motion, self)
self.cnxids.new(self.vte, 'drag-data-received',
self.on_drag_data_received, self)
self.cnxids.new(self.vte, 'selection-changed',
lambda widget: self.maybe_copy_clipboard())
if self.composite_support:
self.cnxids.new(self.vte, 'composited-changed', self.reconfigure)
self.cnxids.new(self.vte, 'window-title-changed', lambda x:
self.emit('title-change', self.get_window_title()))
self.cnxids.new(self.vte, 'grab-focus', self.on_vte_focus)
self.cnxids.new(self.vte, 'focus-in-event', self.on_vte_focus_in)
self.cnxids.new(self.vte, 'focus-out-event', self.on_vte_focus_out)
self.cnxids.new(self.vte, 'size-allocate', self.deferred_on_vte_size_allocate)
self.vte.add_events(Gdk.EventMask.ENTER_NOTIFY_MASK)
self.cnxids.new(self.vte, 'enter_notify_event',
self.on_vte_notify_enter)
self.cnxids.new(self.vte, 'realize', self.reconfigure)
def create_popup_group_menu(self, widget, event = None):
"""Pop up a menu for the group widget"""
if event:
button = event.button
time = event.time
else:
button = 0
time = 0
menu = self.populate_group_menu()
menu.show_all()
menu.popup(None, None, self.position_popup_group_menu, widget, button, time)
return(True)
def populate_group_menu(self):
"""Fill out a group menu"""
menu = Gtk.Menu()
self.group_menu = menu
groupitems = []
item = Gtk.MenuItem.new_with_mnemonic(_('N_ew group...'))
item.connect('activate', self.create_group)
menu.append(item)
if len(self.terminator.groups) > 0:
cnxs = []
item = Gtk.RadioMenuItem.new_with_mnemonic(groupitems, _('_None'))
groupitems = item.get_group()
item.set_active(self.group == None)
cnxs.append([item, 'toggled', self.set_group, None])
menu.append(item)
for group in self.terminator.groups:
item = Gtk.RadioMenuItem.new_with_label(groupitems, group)
groupitems = item.get_group()
item.set_active(self.group == group)
cnxs.append([item, 'toggled', self.set_group, group])
menu.append(item)
for cnx in cnxs:
cnx[0].connect(cnx[1], cnx[2], cnx[3])
if self.group != None or len(self.terminator.groups) > 0:
menu.append(Gtk.SeparatorMenuItem())
if self.group != None:
item = Gtk.MenuItem(_('Remove group %s') % self.group)
item.connect('activate', self.ungroup, self.group)
menu.append(item)
if util.has_ancestor(self, Gtk.Notebook):
item = Gtk.MenuItem.new_with_mnemonic(_('G_roup all in tab'))
item.connect('activate', lambda x: self.emit('group_tab'))
menu.append(item)
if len(self.terminator.groups) > 0:
item = Gtk.MenuItem.new_with_mnemonic(_('Ungro_up all in tab'))
item.connect('activate', lambda x: self.emit('ungroup_tab'))
menu.append(item)
if len(self.terminator.groups) > 0:
item = Gtk.MenuItem(_('Remove all groups'))
item.connect('activate', lambda x: self.emit('ungroup-all'))
menu.append(item)
if self.group != None:
menu.append(Gtk.SeparatorMenuItem())
item = Gtk.MenuItem(_('Close group %s') % self.group)
item.connect('activate', lambda x:
self.terminator.closegroupedterms(self.group))
menu.append(item)
menu.append(Gtk.SeparatorMenuItem())
groupitems = []
cnxs = []
for key, value in {_('Broadcast _all'):'all',
_('Broadcast _group'):'group',
_('Broadcast _off'):'off'}.items():
item = Gtk.RadioMenuItem.new_with_mnemonic(groupitems, key)
groupitems = item.get_group()
dbg('Terminal::populate_group_menu: %s active: %s' %
(key, self.terminator.groupsend ==
self.terminator.groupsend_type[value]))
item.set_active(self.terminator.groupsend ==
self.terminator.groupsend_type[value])
cnxs.append([item, 'activate', self.set_groupsend, self.terminator.groupsend_type[value]])
menu.append(item)
for cnx in cnxs:
cnx[0].connect(cnx[1], cnx[2], cnx[3])
menu.append(Gtk.SeparatorMenuItem())
item = Gtk.CheckMenuItem.new_with_mnemonic(_('_Split to this group'))
item.set_active(self.config['split_to_group'])
item.connect('toggled', lambda x: self.do_splittogroup_toggle())
menu.append(item)
item = Gtk.CheckMenuItem.new_with_mnemonic(_('Auto_clean groups'))
item.set_active(self.config['autoclean_groups'])
item.connect('toggled', lambda x: self.do_autocleangroups_toggle())
menu.append(item)
menu.append(Gtk.SeparatorMenuItem())
item = Gtk.MenuItem.new_with_mnemonic(_('_Insert terminal number'))
item.connect('activate', lambda x: self.emit('enumerate', False))
menu.append(item)
item = Gtk.MenuItem.new_with_mnemonic(_('Insert _padded terminal number'))
item.connect('activate', lambda x: self.emit('enumerate', True))
menu.append(item)
return(menu)
def position_popup_group_menu(self, menu, *args):
"""Calculate the position of the group popup menu"""
# GTK API, or GIR just changed the args. See LP#1518058
widget = args[-1]
_screen_w = Gdk.Screen.width()
screen_h = Gdk.Screen.height()
widget_win = widget.get_window()
_something, widget_x, widget_y = widget_win.get_origin()
_widget_w = widget_win.get_width()
widget_h = widget_win.get_height()
_menu_w = menu.size_request().width
menu_h = menu.size_request().height
if widget_y + widget_h + menu_h > screen_h:
menu_y = max(widget_y - menu_h, 0)
else:
menu_y = widget_y + widget_h
return(widget_x, menu_y, 1)
def set_group(self, _item, name):
"""Set a particular group"""
if self.group == name:
# already in this group, no action needed
return
dbg('Terminal::set_group: Setting group to %s' % name)
self.group = name
self.titlebar.set_group_label(name)
self.terminator.group_hoover()
def create_group(self, _item):
"""Trigger the creation of a group via the titlebar (because popup
windows are really lame)"""
self.titlebar.create_group()
def really_create_group(self, _widget, groupname):
"""The titlebar has spoken, let a group be created"""
self.terminator.create_group(groupname)
self.set_group(None, groupname)
def ungroup(self, _widget, data):
"""Remove a group"""
# FIXME: Could we emit and have Terminator do this?
for term in self.terminator.terminals:
if term.group == data:
term.set_group(None, None)
self.terminator.group_hoover()
def set_groupsend(self, _widget, value):
"""Set the groupsend mode"""
# FIXME: Can we think of a smarter way of doing this than poking?
if value in self.terminator.groupsend_type.values():
dbg('Terminal::set_groupsend: setting groupsend to %s' % value)
self.terminator.groupsend = value
def do_splittogroup_toggle(self):
"""Toggle the splittogroup mode"""
self.config['split_to_group'] = not self.config['split_to_group']
def do_autocleangroups_toggle(self):
"""Toggle the autocleangroups mode"""
self.config['autoclean_groups'] = not self.config['autoclean_groups']
def reconfigure(self, _widget=None):
"""Reconfigure our settings"""
dbg('Terminal::reconfigure')
self.cnxids.remove_signal(self.vte, 'realize')
# Handle child command exiting
self.cnxids.remove_signal(self.vte, 'child-exited')
if self.config['exit_action'] == 'restart':
self.cnxids.new(self.vte, 'child-exited', self.spawn_child, True)
elif self.config['exit_action'] in ('close', 'left'):
self.cnxids.new(self.vte, 'child-exited',
lambda x, y: self.emit('close-term'))
if self.custom_encoding != True:
self.vte.set_encoding(self.config['encoding'])
# Word char support was missing from vte 0.38, silently skip this setting
if hasattr(self.vte, 'set_word_char_exceptions'):
self.vte.set_word_char_exceptions(self.config['word_chars'])
self.vte.set_mouse_autohide(self.config['mouse_autohide'])
backspace = self.config['backspace_binding']
delete = self.config['delete_binding']
try:
if backspace == 'ascii-del':
backbind = Vte.ERASE_ASCII_DELETE
elif backspace == 'control-h':
backbind = Vte.ERASE_ASCII_BACKSPACE
elif backspace == 'escape-sequence':
backbind = Vte.ERASE_DELETE_SEQUENCE
else:
backbind = Vte.ERASE_AUTO
except AttributeError:
if backspace == 'ascii-del':
backbind = 2
elif backspace == 'control-h':
backbind = 1
elif backspace == 'escape-sequence':
backbind = 3
else:
backbind = 0
try:
if delete == 'ascii-del':
delbind = Vte.ERASE_ASCII_DELETE
elif delete == 'control-h':
delbind = Vte.ERASE_ASCII_BACKSPACE
elif delete == 'escape-sequence':
delbind = Vte.ERASE_DELETE_SEQUENCE
else:
delbind = Vte.ERASE_AUTO
except AttributeError:
if delete == 'ascii-del':
delbind = 2
elif delete == 'control-h':
delbind = 1
elif delete == 'escape-sequence':
delbind = 3
else:
delbind = 0
self.vte.set_backspace_binding(backbind)
self.vte.set_delete_binding(delbind)
if not self.custom_font_size:
try:
if self.config['use_system_font'] == True:
font = self.config.get_system_mono_font()
else:
font = self.config['font']
self.set_font(Pango.FontDescription(font))
except:
pass
self.vte.set_allow_bold(self.config['allow_bold'])
if self.config['use_theme_colors']:
self.fgcolor_active = self.vte.get_style_context().get_color(Gtk.StateType.NORMAL) # VERIFY FOR GTK3: do these really take the theme colors?
self.bgcolor = self.vte.get_style_context().get_background_color(Gtk.StateType.NORMAL)
else:
self.fgcolor_active = Gdk.RGBA()
self.fgcolor_active.parse(self.config['foreground_color'])
self.bgcolor = Gdk.RGBA()
self.bgcolor.parse(self.config['background_color'])
if self.config['background_type'] == 'transparent':
self.bgcolor.alpha = self.config['background_darkness']
else:
self.bgcolor.alpha = 1
factor = self.config['inactive_color_offset']
if factor > 1.0:
factor = 1.0
self.fgcolor_inactive = self.fgcolor_active.copy()
dbg(("fgcolor_inactive set to: RGB(%s,%s,%s)", getattr(self.fgcolor_inactive, "red"),
getattr(self.fgcolor_inactive, "green"),
getattr(self.fgcolor_inactive, "blue")))
for bit in ['red', 'green', 'blue']:
setattr(self.fgcolor_inactive, bit,
getattr(self.fgcolor_inactive, bit) * factor)
dbg(("fgcolor_inactive set to: RGB(%s,%s,%s)", getattr(self.fgcolor_inactive, "red"),
getattr(self.fgcolor_inactive, "green"),
getattr(self.fgcolor_inactive, "blue")))
colors = self.config['palette'].split(':')
self.palette_active = []
for color in colors:
if color:
newcolor = Gdk.RGBA()
newcolor.parse(color)
self.palette_active.append(newcolor)
if len(colors) == 16:
# RGB values for indices 16..255 copied from vte source in order to dim them
shades = [0, 95, 135, 175, 215, 255]
for r in xrange(0, 6):
for g in xrange(0, 6):
for b in xrange(0, 6):
newcolor = Gdk.RGBA()
setattr(newcolor, "red", shades[r] / 255.0)
setattr(newcolor, "green", shades[g] / 255.0)
setattr(newcolor, "blue", shades[b] / 255.0)
self.palette_active.append(newcolor)
for y in xrange(8, 248, 10):
newcolor = Gdk.RGBA()
setattr(newcolor, "red", y / 255.0)
setattr(newcolor, "green", y / 255.0)
setattr(newcolor, "blue", y / 255.0)
self.palette_active.append(newcolor)
self.palette_inactive = []
for color in self.palette_active:
newcolor = Gdk.RGBA()
for bit in ['red', 'green', 'blue']:
setattr(newcolor, bit,
getattr(color, bit) * factor)
self.palette_inactive.append(newcolor)
if self.terminator.last_focused_term == self:
self.vte.set_colors(self.fgcolor_active, self.bgcolor,
self.palette_active)
else:
self.vte.set_colors(self.fgcolor_inactive, self.bgcolor,
self.palette_inactive)
profiles = self.config.base.profiles
terminal_box_style_context = self.terminalbox.get_style_context()
for profile in profiles.keys():
munged_profile = "terminator-profile-%s" % (
"".join([c if c.isalnum() else "-" for c in profile]))
if terminal_box_style_context.has_class(munged_profile):
terminal_box_style_context.remove_class(munged_profile)
munged_profile = "".join([c if c.isalnum() else "-" for c in self.get_profile()])
css_class_name = "terminator-profile-%s" % (munged_profile)
terminal_box_style_context.add_class(css_class_name)
self.set_cursor_color()
self.vte.set_cursor_shape(getattr(Vte.CursorShape,
self.config['cursor_shape'].upper()));
if self.config['cursor_blink'] == True:
self.vte.set_cursor_blink_mode(Vte.CursorBlinkMode.ON)
else:
self.vte.set_cursor_blink_mode(Vte.CursorBlinkMode.OFF)
if self.config['force_no_bell'] == True:
self.vte.set_audible_bell(False)
self.cnxids.remove_signal(self.vte, 'bell')
else:
self.vte.set_audible_bell(self.config['audible_bell'])
self.cnxids.remove_signal(self.vte, 'bell')
if self.config['urgent_bell'] == True or \
self.config['icon_bell'] == True or \
self.config['visible_bell'] == True:
try:
self.cnxids.new(self.vte, 'bell', self.on_bell)
except TypeError:
err('bell signal unavailable with this version of VTE')
if self.config['scrollback_infinite'] == True:
scrollback_lines = -1
else:
scrollback_lines = self.config['scrollback_lines']
self.vte.set_scrollback_lines(scrollback_lines)
self.vte.set_scroll_on_keystroke(self.config['scroll_on_keystroke'])
self.vte.set_scroll_on_output(self.config['scroll_on_output'])
if self.config['scrollbar_position'] in ['disabled', 'hidden']:
self.scrollbar.hide()
else:
self.scrollbar.show()
if self.config['scrollbar_position'] == 'left':
self.terminalbox.reorder_child(self.scrollbar, 0)
elif self.config['scrollbar_position'] == 'right':
self.terminalbox.reorder_child(self.vte, 0)
self.vte.set_rewrap_on_resize(self.config['rewrap_on_resize'])
self.titlebar.update()
self.vte.queue_draw()
def set_cursor_color(self):
"""Set the cursor color appropriately"""
if self.config['cursor_color_fg']:
self.vte.set_color_cursor(None)
else:
cursor_color = Gdk.RGBA()
cursor_color.parse(self.config['cursor_color'])
self.vte.set_color_cursor(cursor_color)
def get_window_title(self):
"""Return the window title"""
return(self.vte.get_window_title() or str(self.command))
def on_group_button_press(self, widget, event):
"""Handler for the group button"""
if event.button == 1:
if event.type == Gdk.EventType._2BUTTON_PRESS or \
event.type == Gdk.EventType._3BUTTON_PRESS:
# Ignore these, or they make the interaction bad
return True
# Super key applies interaction to all terms in group
include_siblings=event.get_state() & Gdk.ModifierType.MOD4_MASK == Gdk.ModifierType.MOD4_MASK
if include_siblings:
targets=self.terminator.get_sibling_terms(self)
else:
targets=[self]
if event.get_state() & Gdk.ModifierType.CONTROL_MASK == Gdk.ModifierType.CONTROL_MASK:
dbg('on_group_button_press: toggle terminal to focused terminals group')
focused=self.get_toplevel().get_focussed_terminal()
if focused in targets: targets.remove(focused)
if self != focused:
if self.group==focused.group:
new_group=None
else:
new_group=focused.group
[term.set_group(None, new_group) for term in targets]
[term.titlebar.update(focused) for term in targets]
return True
elif event.get_state() & Gdk.ModifierType.SHIFT_MASK == Gdk.ModifierType.SHIFT_MASK:
dbg('on_group_button_press: rename of terminals group')
self.targets_for_new_group = targets
self.titlebar.create_group()
return True
elif event.type == Gdk.EventType.BUTTON_PRESS:
# Single Click gives popup
dbg('on_group_button_press: group menu popup')
self.create_popup_group_menu(widget, event)
return True
else:
dbg('on_group_button_press: unknown group button interaction')
return(False)
def on_keypress(self, widget, event):
"""Handler for keyboard events"""
if not event:
dbg('Terminal::on_keypress: Called on %s with no event' % widget)
return(False)
# Workaround for IBus interfering with broadcast when using dead keys
# Environment also needs IBUS_DISABLE_SNOOPER=1, or double chars appear
# in the receivers.
if self.terminator.ibus_running:
if (event.state | Gdk.ModifierType.MODIFIER_MASK ) ^ Gdk.ModifierType.MODIFIER_MASK != 0:
dbg('Terminal::on_keypress: Ingore processed event with event.state %d' % event.state)
return(False)
# FIXME: Does keybindings really want to live in Terminator()?
mapping = self.terminator.keybindings.lookup(event)
if mapping == "hide_window":
return(False)
if mapping and mapping not in ['close_window',
'full_screen']:
dbg('Terminal::on_keypress: lookup found: %r' % mapping)
# handle the case where user has re-bound copy to ctrl+
# we only copy if there is a selection otherwise let it fall through
# to ^
if (mapping == "copy" and event.get_state() & Gdk.ModifierType.CONTROL_MASK):
if self.vte.get_has_selection ():
getattr(self, "key_" + mapping)()
return(True)
elif not self.config['smart_copy']:
return(True)
else:
getattr(self, "key_" + mapping)()
return(True)
# FIXME: This is all clearly wrong. We should be doing this better
# maybe we can emit the key event and let Terminator() care?
groupsend = self.terminator.groupsend
groupsend_type = self.terminator.groupsend_type
window_focussed = self.vte.get_toplevel().get_property('has-toplevel-focus')
if groupsend != groupsend_type['off'] and window_focussed and self.vte.is_focus():
if self.group and groupsend == groupsend_type['group']:
self.terminator.group_emit(self, self.group, 'key-press-event',
event)
if groupsend == groupsend_type['all']:
self.terminator.all_emit(self, 'key-press-event', event)
return(False)
def on_buttonpress(self, widget, event):
"""Handler for mouse events"""
# Any button event should grab focus
widget.grab_focus()
if type(widget) == Gtk.VScrollbar and event.type == Gdk.EventType._2BUTTON_PRESS:
# Suppress double-click behavior
return True
use_primary = (display_manager() != 'WAYLAND')
if self.config['putty_paste_style']:
middle_click = [self.popup_menu, (widget, event)]
right_click = [self.paste_clipboard, (use_primary, )]
else:
middle_click = [self.paste_clipboard, (use_primary, )]
right_click = [self.popup_menu, (widget, event)]
if event.button == self.MOUSEBUTTON_LEFT:
# Ctrl+leftclick on a URL should open it
if event.get_state() & Gdk.ModifierType.CONTROL_MASK == Gdk.ModifierType.CONTROL_MASK:
url = self.vte.match_check_event(event)
if url[0]:
self.open_url(url, prepare=True)
elif event.button == self.MOUSEBUTTON_MIDDLE:
# middleclick should paste the clipboard
# try to pass it to vte widget first though
if event.get_state() & Gdk.ModifierType.CONTROL_MASK == 0:
if event.get_state() & Gdk.ModifierType.SHIFT_MASK == 0:
if not Vte.Terminal.do_button_press_event(self.vte, event):
middle_click[0](*middle_click[1])
else:
middle_click[0](*middle_click[1])
return(True)
return Vte.Terminal.do_button_press_event(self.vte, event)
#return(True)
elif event.button == self.MOUSEBUTTON_RIGHT:
# rightclick should display a context menu if Ctrl is not pressed,
# plus either the app is not interested in mouse events or Shift is pressed
if event.get_state() & Gdk.ModifierType.CONTROL_MASK == 0:
if event.get_state() & Gdk.ModifierType.SHIFT_MASK == 0:
if not Vte.Terminal.do_button_press_event(self.vte, event):
right_click[0](*right_click[1])
else:
right_click[0](*right_click[1])
return(True)
return(False)
def on_mousewheel(self, widget, event):
"""Handler for modifier + mouse wheel scroll events"""
SMOOTH_SCROLL_UP = event.direction == Gdk.ScrollDirection.SMOOTH and event.delta_y <= 0.
SMOOTH_SCROLL_DOWN = event.direction == Gdk.ScrollDirection.SMOOTH and event.delta_y > 0.
if event.state & Gdk.ModifierType.CONTROL_MASK == Gdk.ModifierType.CONTROL_MASK:
# Ctrl + mouse wheel up/down with Shift and Super additions
if event.state & Gdk.ModifierType.MOD4_MASK == Gdk.ModifierType.MOD4_MASK:
targets=self.terminator.terminals
elif event.state & Gdk.ModifierType.SHIFT_MASK == Gdk.ModifierType.SHIFT_MASK:
targets=self.terminator.get_target_terms(self)
else:
targets=[self]
if event.direction == Gdk.ScrollDirection.UP or SMOOTH_SCROLL_UP:
for target in targets:
target.zoom_in()
return (True)
elif event.direction == Gdk.ScrollDirection.DOWN or SMOOTH_SCROLL_DOWN:
for target in targets:
target.zoom_out()
return (True)
if event.state & Gdk.ModifierType.SHIFT_MASK == Gdk.ModifierType.SHIFT_MASK:
# Shift + mouse wheel up/down
if event.direction == Gdk.ScrollDirection.UP or SMOOTH_SCROLL_UP:
self.scroll_by_page(-1)
return (True)
elif event.direction == Gdk.ScrollDirection.DOWN or SMOOTH_SCROLL_DOWN:
self.scroll_by_page(1)
return (True)
return(False)
def popup_menu(self, widget, event=None):
"""Display the context menu"""
menu = TerminalPopupMenu(self)
menu.show(widget, event)
def do_scrollbar_toggle(self):
"""Show or hide the terminal scrollbar"""
self.toggle_widget_visibility(self.scrollbar)
def toggle_widget_visibility(self, widget):
"""Show or hide a widget"""
if widget.get_property('visible'):
widget.hide()
else:
widget.show()
def on_encoding_change(self, _widget, encoding):
"""Handle the encoding changing"""
current = self.vte.get_encoding()
if current != encoding:
dbg('on_encoding_change: setting encoding to: %s' % encoding)
self.custom_encoding = not (encoding == self.config['encoding'])
self.vte.set_encoding(encoding)
def on_drag_begin(self, widget, drag_context, _data):
"""Handle the start of a drag event"""
Gtk.drag_set_icon_pixbuf(drag_context, util.widget_pixbuf(self, 512), 0, 0)
def on_drag_data_get(self, _widget, _drag_context, selection_data, info,
_time, data):
"""I have no idea what this does, drag and drop is a mystery. sorry."""
selection_data.set(Gdk.atom_intern('vte', False), info,
str(data.terminator.terminals.index(self)))
def on_drag_motion(self, widget, drag_context, x, y, _time, _data):
"""*shrug*"""
if not drag_context.list_targets() == [Gdk.atom_intern('vte', False)] and \
(Gtk.targets_include_text(drag_context.list_targets()) or \
Gtk.targets_include_uri(drag_context.list_targets())):
# copy text from another widget
return
srcwidget = Gtk.drag_get_source_widget(drag_context)
if(isinstance(srcwidget, Gtk.EventBox) and
srcwidget == self.titlebar) or widget == srcwidget:
# on self
return
alloc = widget.get_allocation()
if self.config['use_theme_colors']:
color = self.vte.get_style_context().get_color(Gtk.StateType.NORMAL) # VERIFY FOR GTK3 as above
else:
color = Gdk.RGBA()
color.parse(self.config['foreground_color']) # VERIFY FOR GTK3
pos = self.get_location(widget, x, y)
topleft = (0, 0)
topright = (alloc.width, 0)
topmiddle = (alloc.width/2, 0)
bottomleft = (0, alloc.height)
bottomright = (alloc.width, alloc.height)
bottommiddle = (alloc.width/2, alloc.height)
middleleft = (0, alloc.height/2)
middleright = (alloc.width, alloc.height/2)
#print "%f %f %d %d" %(coef1, coef2, b1,b2)
coord = ()
if pos == "right":
coord = (topright, topmiddle, bottommiddle, bottomright)
elif pos == "top":
coord = (topleft, topright, middleright , middleleft)
elif pos == "left":
coord = (topleft, topmiddle, bottommiddle, bottomleft)
elif pos == "bottom":
coord = (bottomleft, bottomright, middleright , middleleft)
#here, we define some widget internal values
widget._draw_data = { 'color': color, 'coord' : coord }
#redraw by forcing an event
connec = widget.connect_after('draw', self.on_draw)
widget.queue_draw_area(0, 0, alloc.width, alloc.height)
widget.get_window().process_updates(True)
#finaly reset the values
widget.disconnect(connec)
widget._draw_data = None
def on_draw(self, widget, context):
"""Handle an expose event while dragging"""
if not widget._draw_data:
return(False)
color = widget._draw_data['color']
coord = widget._draw_data['coord']
context.set_source_rgba(color.red, color.green, color.blue, 0.5)
if len(coord) > 0 :
context.move_to(coord[len(coord)-1][0], coord[len(coord)-1][1])
for i in coord:
context.line_to(i[0], i[1])
context.fill()
return(False)
def on_drag_data_received(self, widget, drag_context, x, y, selection_data,
info, _time, data):
"""Something has been dragged into the terminal. Handle it as either a
URL or another terminal."""
dbg('drag data received of type: %s' % (selection_data.get_data_type()))
if Gtk.targets_include_text(drag_context.list_targets()) or \
Gtk.targets_include_uri(drag_context.list_targets()):
# copy text with no modification yet to destination
txt = selection_data.get_data()
# https://bugs.launchpad.net/terminator/+bug/1518705
if info == self.TARGET_TYPE_MOZ:
txt = txt.decode('utf-16').encode('utf-8')
txt = txt.split('\n')[0]
txt_lines = txt.split( "\r\n" )
if txt_lines[-1] == '':
for line in txt_lines[:-1]:
if line[0:7] != 'file://':
txt = txt.replace('\r\n','\n')
break
else:
# It is a list of crlf terminated file:// URL. let's
# iterate over all elements except the last one.
str=''
for fname in txt_lines[:-1]:
dbg('drag data fname: %s' % fname)
fname = "'%s'" % urllib.unquote(fname[7:].replace("'",
'\'\\\'\''))
str += fname + ' '
txt=str
for term in self.terminator.get_target_terms(self):
term.feed(txt)
return
widgetsrc = data.terminator.terminals[int(selection_data.get_data())]
srcvte = Gtk.drag_get_source_widget(drag_context)
#check if computation requireds
if (isinstance(srcvte, Gtk.EventBox) and
srcvte == self.titlebar) or srcvte == widget:
return
srchbox = widgetsrc
# The widget argument is actually a Vte.Terminal(). Turn that into a
# terminatorlib Terminal()
maker = Factory()
while True:
widget = widget.get_parent()
if not widget:
# We've run out of widgets. Something is wrong.
err('Failed to find Terminal from vte')
return
if maker.isinstance(widget, 'Terminal'):
break
dsthbox = widget
dstpaned = dsthbox.get_parent()
srcpaned = srchbox.get_parent()
pos = self.get_location(widget, x, y)
srcpaned.remove(widgetsrc)
dstpaned.split_axis(dsthbox, pos in ['top', 'bottom'], None, widgetsrc, pos in ['bottom', 'right'])
srcpaned.hoover()
widgetsrc.ensure_visible_and_focussed()
def get_location(self, term, x, y):
"""Get our location within the terminal"""
pos = ''
#get the diagonales function for the receiving widget
term_alloc = term.get_allocation()
coef1 = float(term_alloc.height)/float(term_alloc.width)
coef2 = -float(term_alloc.height)/float(term_alloc.width)
b1 = 0
b2 = term_alloc.height
#determine position in rectangle
#--------
#|\ /|
#| \ / |
#| \/ |
#| /\ |
#| / \ |
#|/ \|
#--------
if (x*coef1 + b1 > y ) and (x*coef2 + b2 < y ):
pos = "right"
if (x*coef1 + b1 > y ) and (x*coef2 + b2 > y ):
pos = "top"
if (x*coef1 + b1 < y ) and (x*coef2 + b2 > y ):
pos = "left"
if (x*coef1 + b1 < y ) and (x*coef2 + b2 < y ):
pos = "bottom"
return pos
def grab_focus(self):
"""Steal focus for this terminal"""
if not self.vte.has_focus():
self.vte.grab_focus()
def ensure_visible_and_focussed(self):
"""Make sure that we're visible and focussed"""
window = self.get_toplevel()
topchild = window.get_children()[0]
maker = Factory()
if maker.isinstance(topchild, 'Notebook'):
# Find which page number this term is on
tabnum = topchild.page_num_descendant(self)
# If terms page number is not the current one, switch to it
current_page = topchild.get_current_page()
if tabnum != current_page:
topchild.set_current_page(tabnum)
self.grab_focus()
def on_vte_focus(self, _widget):
"""Update our UI when we get focus"""
self.emit('title-change', self.get_window_title())
def on_vte_focus_in(self, _widget, _event):
"""Inform other parts of the application when focus is received"""
self.vte.set_colors(self.fgcolor_active, self.bgcolor,
self.palette_active)
self.set_cursor_color()
if not self.terminator.doing_layout:
self.terminator.last_focused_term = self
if self.get_toplevel().is_child_notebook():
notebook = self.get_toplevel().get_children()[0]
notebook.set_last_active_term(self.uuid)
notebook.clean_last_active_term()
self.get_toplevel().last_active_term = None
else:
self.get_toplevel().last_active_term = self.uuid
self.emit('focus-in')
def on_vte_focus_out(self, _widget, _event):
"""Inform other parts of the application when focus is lost"""
self.vte.set_colors(self.fgcolor_inactive, self.bgcolor,
self.palette_inactive)
self.set_cursor_color()
self.emit('focus-out')
def on_window_focus_out(self):
"""Update our UI when the window loses focus"""
self.titlebar.update('window-focus-out')
def scrollbar_jump(self, position):
"""Move the scrollbar to a particular row"""
self.scrollbar.set_value(position)
def on_search_done(self, _widget):
"""We've finished searching, so clean up"""
self.searchbar.hide()
self.scrollbar.set_value(self.vte.get_cursor_position()[1])
self.vte.grab_focus()
def on_edit_done(self, _widget):
"""A child widget is done editing a label, return focus to VTE"""
self.vte.grab_focus()
def deferred_on_vte_size_allocate(self, widget, allocation):
# widget & allocation are not used in on_vte_size_allocate, so we
# can use the on_vte_size_allocate instead of duplicating the code
if self.pending_on_vte_size_allocate == True:
return
self.pending_on_vte_size_allocate = True
GObject.idle_add(self.do_deferred_on_vte_size_allocate, widget, allocation)
def do_deferred_on_vte_size_allocate(self, widget, allocation):
self.pending_on_vte_size_allocate = False
self.on_vte_size_allocate(widget, allocation)
def on_vte_size_allocate(self, widget, allocation):
self.titlebar.update_terminal_size(self.vte.get_column_count(),
self.vte.get_row_count())
if self.config['geometry_hinting']:
window = self.get_toplevel()
window.deferred_set_rough_geometry_hints()
def on_vte_notify_enter(self, term, event):
"""Handle the mouse entering this terminal"""
# FIXME: This shouldn't be looking up all these values every time
sloppy = False
if self.config['focus'] == 'system':
sloppy = self.config.get_system_focus() in ['sloppy', 'mouse']
elif self.config['focus'] in ['sloppy', 'mouse']:
sloppy = True
if sloppy == True and self.titlebar.editing() == False:
term.grab_focus()
return(False)
def get_zoom_data(self):
"""Return a dict of information for Window"""
data = {}
data['old_font'] = self.vte.get_font().copy()
data['old_char_height'] = self.vte.get_char_height()
data['old_char_width'] = self.vte.get_char_width()
data['old_allocation'] = self.vte.get_allocation()
data['old_columns'] = self.vte.get_column_count()
data['old_rows'] = self.vte.get_row_count()
data['old_parent'] = self.get_parent()
return(data)
def zoom_scale(self, widget, allocation, old_data):
"""Scale our font correctly based on how big we are not vs before"""
self.cnxids.remove_signal(self, 'size-allocate')
# FIXME: Is a zoom signal actualy used anywhere?
self.cnxids.remove_signal(self, 'zoom')
new_columns = self.vte.get_column_count()
new_rows = self.vte.get_row_count()
new_font = self.vte.get_font()
dbg('Terminal::zoom_scale: Resized from %dx%d to %dx%d' % (
old_data['old_columns'],
old_data['old_rows'],
new_columns,
new_rows))
if new_rows == old_data['old_rows'] or \
new_columns == old_data['old_columns']:
dbg('Terminal::zoom_scale: One axis unchanged, not scaling')
return
scale_factor = min ( (new_columns / old_data['old_columns'] * 0.97),
(new_rows / old_data['old_rows'] * 1.05) )
new_size = int(old_data['old_font'].get_size() * scale_factor)
if new_size == 0:
err('refusing to set a zero sized font')
return
new_font.set_size(new_size)
dbg('setting new font: %s' % new_font)
self.set_font(new_font)
def is_zoomed(self):
"""Determine if we are a zoomed terminal"""
prop = None
window = self.get_toplevel()
try:
prop = window.get_property('term-zoomed')
except TypeError:
prop = False
return(prop)
def zoom(self, widget=None):
"""Zoom ourself to fill the window"""
self.emit('zoom')
def maximise(self, widget=None):
"""Maximise ourself to fill the window"""
self.emit('maximise')
def unzoom(self, widget=None):
"""Restore normal layout"""
self.emit('unzoom')
def set_cwd(self, cwd=None):
"""Set our cwd"""
if cwd is not None:
self.cwd = cwd
def spawn_child(self, widget=None, respawn=False, debugserver=False):
args = []
shell = None
command = None
if self.terminator.doing_layout == True:
dbg('still laying out, refusing to spawn a child')
return
if respawn == False:
self.vte.grab_focus()
options = self.config.options_get()
if options and options.command:
command = options.command
options.command = None
elif options and options.execute:
command = options.execute
options.execute = None
elif self.config['use_custom_command']:
command = self.config['custom_command']
elif self.layout_command:
command = self.layout_command
elif debugserver is True:
details = self.terminator.debug_address
dbg('spawning debug session with: %s:%s' % (details[0],
details[1]))
command = 'telnet %s %s' % (details[0], details[1])
# working directory set in layout config
if self.directory:
self.set_cwd(self.directory)
# working directory given as argument
elif options and options.working_directory and \
options.working_directory != '':
self.set_cwd(options.working_directory)
options.working_directory = ''
if type(command) is list:
shell = util.path_lookup(command[0])
args = command
else:
shell = util.shell_lookup()
if self.config['login_shell']:
args.insert(0, "-%s" % shell)
else:
args.insert(0, shell)
if command is not None:
args += ['-c', command]
if shell is None:
self.vte.feed(_('Unable to find a shell'))
return(-1)
try:
os.putenv('WINDOWID', '%s' % self.vte.get_parent_window().xid)
except AttributeError:
pass
envv = []
envv.append('TERM=%s' % self.config['term'])
envv.append('COLORTERM=%s' % self.config['colorterm'])
envv.append('PWD=%s' % self.cwd)
envv.append('TERMINATOR_UUID=%s' % self.uuid.urn)
if self.terminator.dbus_name:
envv.append('TERMINATOR_DBUS_NAME=%s' % self.terminator.dbus_name)
if self.terminator.dbus_path:
envv.append('TERMINATOR_DBUS_PATH=%s' % self.terminator.dbus_path)
dbg('Forking shell: "%s" with args: %s' % (shell, args))
args.insert(0, shell)
result, self.pid = self.vte.spawn_sync(Vte.PtyFlags.DEFAULT,
self.cwd,
args,
envv,
GLib.SpawnFlags.FILE_AND_ARGV_ZERO | GLib.SpawnFlags.DO_NOT_REAP_CHILD,
None,
None,
None)
self.command = shell
self.titlebar.update()
if self.pid == -1:
self.vte.feed(_('Unable to start shell:') + shell)
return(-1)
def prepare_url(self, urlmatch):
"""Prepare a URL from a VTE match"""
url = urlmatch[0]
match = urlmatch[1]
if match == self.matches['email'] and url[0:7] != 'mailto:':
url = 'mailto:' + url
elif match == self.matches['addr_only'] and url[0:3] == 'ftp':
url = 'ftp://' + url
elif match == self.matches['addr_only']:
url = 'http://' + url
elif match in self.matches.values():
# We have a match, but it's not a hard coded one, so it's a plugin
try:
registry = plugin.PluginRegistry()
registry.load_plugins()
plugins = registry.get_plugins_by_capability('url_handler')
for urlplugin in plugins:
if match == self.matches[urlplugin.handler_name]:
newurl = urlplugin.callback(url)
if newurl is not None:
dbg('Terminal::prepare_url: URL prepared by \
%s plugin' % urlplugin.handler_name)
url = newurl
break
except Exception, ex:
err('Exception occurred preparing URL: %s' % ex)
return(url)
def open_url(self, url, prepare=False):
"""Open a given URL, conditionally unpacking it from a VTE match"""
if prepare == True:
url = self.prepare_url(url)
dbg('open_url: URL: %s (prepared: %s)' % (url, prepare))
if self.config['use_custom_url_handler']:
dbg("Using custom URL handler: %s" %
self.config['custom_url_handler'])
try:
subprocess.Popen([self.config['custom_url_handler'], url])
return
except:
dbg('custom url handler did not work, falling back to defaults')
try:
Gtk.show_uri(None, url, Gdk.CURRENT_TIME)
return
except:
dbg('Gtk.show_uri did not work, falling through to xdg-open')
try:
subprocess.Popen(["xdg-open", url])
except:
dbg('xdg-open did not work, falling back to webbrowser.open')
import webbrowser
webbrowser.open(url)
def paste_clipboard(self, primary=False):
"""Paste one of the two clipboards"""
for term in self.terminator.get_target_terms(self):
if primary:
term.vte.paste_primary()
else:
term.vte.paste_clipboard()
self.vte.grab_focus()
def feed(self, text):
"""Feed the supplied text to VTE"""
self.vte.feed_child(text, len(text))
def zoom_in(self):
"""Increase the font size"""
self.zoom_font(True)
def zoom_out(self):
"""Decrease the font size"""
self.zoom_font(False)
def zoom_font(self, zoom_in):
"""Change the font size"""
pangodesc = self.vte.get_font()
fontsize = pangodesc.get_size()
if fontsize > Pango.SCALE and not zoom_in:
fontsize -= Pango.SCALE
elif zoom_in:
fontsize += Pango.SCALE
pangodesc.set_size(fontsize)
self.set_font(pangodesc)
self.custom_font_size = fontsize
def zoom_orig(self):
"""Restore original font size"""
if self.config['use_system_font'] == True:
font = self.config.get_system_mono_font()
else:
font = self.config['font']
dbg("Terminal::zoom_orig: restoring font to: %s" % font)
self.set_font(Pango.FontDescription(font))
self.custom_font_size = None
def set_font(self, fontdesc):
"""Set the font we want in VTE"""
self.vte.set_font(fontdesc)
def get_cursor_position(self):
"""Return the co-ordinates of our cursor"""
# FIXME: THIS METHOD IS DEPRECATED AND UNUSED
col, row = self.vte.get_cursor_position()
width = self.vte.get_char_width()
height = self.vte.get_char_height()
return((col * width, row * height))
def get_font_size(self):
"""Return the width/height of our font"""
return((self.vte.get_char_width(), self.vte.get_char_height()))
def get_size(self):
"""Return the column/rows of the terminal"""
return((self.vte.get_column_count(), self.vte.get_row_count()))
def on_bell(self, widget):
"""Set the urgency hint/icon/flash for our window"""
if self.config['urgent_bell'] == True:
window = self.get_toplevel()
if window.is_toplevel():
window.set_urgency_hint(True)
if self.config['icon_bell'] == True:
self.titlebar.icon_bell()
if self.config['visible_bell'] == True:
# Repurposed the code used for drag and drop overlay to provide a visual terminal flash
alloc = widget.get_allocation()
if self.config['use_theme_colors']:
color = self.vte.get_style_context().get_color(Gtk.StateType.NORMAL) # VERIFY FOR GTK3 as above
else:
color = Gdk.RGBA()
color.parse(self.config['foreground_color']) # VERIFY FOR GTK3
coord = ( (0, 0), (alloc.width, 0), (alloc.width, alloc.height), (0, alloc.height))
#here, we define some widget internal values
widget._draw_data = { 'color': color, 'coord' : coord }
#redraw by forcing an event
connec = widget.connect_after('draw', self.on_draw)
widget.queue_draw_area(0, 0, alloc.width, alloc.height)
widget.get_window().process_updates(True)
#finaly reset the values
widget.disconnect(connec)
widget._draw_data = None
# Add timeout to clean up display
GObject.timeout_add(100, self.on_bell_cleanup, widget, alloc)
def on_bell_cleanup(self, widget, alloc):
'''Queue a redraw to clear the visual flash overlay'''
widget.queue_draw_area(0, 0, alloc.width, alloc.height)
widget.get_window().process_updates(True)
return False
def describe_layout(self, count, parent, global_layout, child_order):
"""Describe our layout"""
layout = {}
layout['type'] = 'Terminal'
layout['parent'] = parent
layout['order'] = child_order
if self.group:
layout['group'] = self.group
profile = self.get_profile()
if layout != "default":
# There's no point explicitly noting default profiles
layout['profile'] = profile
title = self.titlebar.get_custom_string()
if title:
layout['title'] = title
layout['uuid'] = self.uuid
name = 'terminal%d' % count
count = count + 1
global_layout[name] = layout
return(count)
def create_layout(self, layout):
"""Apply our layout"""
dbg('Setting layout')
if layout.has_key('command') and layout['command'] != '':
self.layout_command = layout['command']
if layout.has_key('profile') and layout['profile'] != '':
if layout['profile'] in self.config.list_profiles():
self.set_profile(self, layout['profile'])
if layout.has_key('group') and layout['group'] != '':
# This doesn't need/use self.titlebar, but it's safer than sending
# None
self.really_create_group(self.titlebar, layout['group'])
if layout.has_key('title') and layout['title'] != '':
self.titlebar.set_custom_string(layout['title'])
if layout.has_key('directory') and layout['directory'] != '':
self.directory = layout['directory']
if layout.has_key('uuid') and layout['uuid'] != '':
self.uuid = make_uuid(layout['uuid'])
def scroll_by_page(self, pages):
"""Scroll up or down in pages"""
amount = pages * self.vte.get_vadjustment().get_page_increment()
self.scroll_by(int(amount))
def scroll_by_line(self, lines):
"""Scroll up or down in lines"""
amount = lines * self.vte.get_vadjustment().get_step_increment()
self.scroll_by(int(amount))
def scroll_by(self, amount):
"""Scroll up or down by an amount of lines"""
adjustment = self.vte.get_vadjustment()
bottom = adjustment.get_upper() - adjustment.get_page_size()
value = adjustment.get_value() + amount
adjustment.set_value(min(value, bottom))
def get_allocation(self):
"""Get a real allocation which includes the bloody x and y coordinates (grumble, grumble)"""
alloc = super(Terminal, self).get_allocation()
rv = self.translate_coordinates(self.get_toplevel(), 0, 0)
if rv:
alloc.x, alloc.y = rv
return alloc
# There now begins a great list of keyboard event handlers
def key_zoom_in(self):
self.zoom_in()
def key_next_profile(self):
self.switch_to_next_profile()
def key_previous_profile(self):
self.switch_to_previous_profile()
def key_zoom_out(self):
self.zoom_out()
def key_copy(self):
self.vte.copy_clipboard()
def key_paste(self):
self.paste_clipboard()
def key_toggle_scrollbar(self):
self.do_scrollbar_toggle()
def key_zoom_normal(self):
self.zoom_orig ()
def key_search(self):
self.searchbar.start_search()
# bindings that should be moved to Terminator as they all just call
# a function of Terminator. It would be cleaner if TerminatorTerm
# has absolutely no reference to Terminator.
# N (next) - P (previous) - O (horizontal) - E (vertical) - W (close)
def key_cycle_next(self):
self.key_go_next()
def key_cycle_prev(self):
self.key_go_prev()
def key_go_next(self):
self.emit('navigate', 'next')
def key_go_prev(self):
self.emit('navigate', 'prev')
def key_go_up(self):
self.emit('navigate', 'up')
def key_go_down(self):
self.emit('navigate', 'down')
def key_go_left(self):
self.emit('navigate', 'left')
def key_go_right(self):
self.emit('navigate', 'right')
def key_split_horiz(self):
self.emit('split-horiz', self.get_cwd())
def key_split_vert(self):
self.emit('split-vert', self.get_cwd())
def key_rotate_cw(self):
self.emit('rotate-cw')
def key_rotate_ccw(self):
self.emit('rotate-ccw')
def key_close_term(self):
self.close()
def key_resize_up(self):
self.emit('resize-term', 'up')
def key_resize_down(self):
self.emit('resize-term', 'down')
def key_resize_left(self):
self.emit('resize-term', 'left')
def key_resize_right(self):
self.emit('resize-term', 'right')
def key_move_tab_right(self):
self.emit('move-tab', 'right')
def key_move_tab_left(self):
self.emit('move-tab', 'left')
def key_toggle_zoom(self):
if self.is_zoomed():
self.unzoom()
else:
self.maximise()
def key_scaled_zoom(self):
if self.is_zoomed():
self.unzoom()
else:
self.zoom()
def key_next_tab(self):
self.emit('tab-change', -1)
def key_prev_tab(self):
self.emit('tab-change', -2)
def key_switch_to_tab_1(self):
self.emit('tab-change', 0)
def key_switch_to_tab_2(self):
self.emit('tab-change', 1)
def key_switch_to_tab_3(self):
self.emit('tab-change', 2)
def key_switch_to_tab_4(self):
self.emit('tab-change', 3)
def key_switch_to_tab_5(self):
self.emit('tab-change', 4)
def key_switch_to_tab_6(self):
self.emit('tab-change', 5)
def key_switch_to_tab_7(self):
self.emit('tab-change', 6)
def key_switch_to_tab_8(self):
self.emit('tab-change', 7)
def key_switch_to_tab_9(self):
self.emit('tab-change', 8)
def key_switch_to_tab_10(self):
self.emit('tab-change', 9)
def key_reset(self):
self.vte.reset (True, False)
def key_reset_clear(self):
self.vte.reset (True, True)
def key_group_all(self):
self.emit('group-all')
def key_group_all_toggle(self):
self.emit('group-all-toggle')
def key_ungroup_all(self):
self.emit('ungroup-all')
def key_group_tab(self):
self.emit('group-tab')
def key_group_tab_toggle(self):
self.emit('group-tab-toggle')
def key_ungroup_tab(self):
self.emit('ungroup-tab')
def key_new_window(self):
self.terminator.new_window(self.get_cwd(), self.get_profile())
def key_new_tab(self):
self.get_toplevel().tab_new(self)
def key_new_terminator(self):
spawn_new_terminator(self.origcwd, ['-u'])
def key_broadcast_off(self):
self.set_groupsend(None, self.terminator.groupsend_type['off'])
self.terminator.focus_changed(self)
def key_broadcast_group(self):
self.set_groupsend(None, self.terminator.groupsend_type['group'])
self.terminator.focus_changed(self)
def key_broadcast_all(self):
self.set_groupsend(None, self.terminator.groupsend_type['all'])
self.terminator.focus_changed(self)
def key_insert_number(self):
self.emit('enumerate', False)
def key_insert_padded(self):
self.emit('enumerate', True)
def key_edit_window_title(self):
window = self.get_toplevel()
dialog = Gtk.Dialog(_('Rename Window'), window,
Gtk.DialogFlags.MODAL,
( Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT,
Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT ))
dialog.set_default_response(Gtk.ResponseType.ACCEPT)
dialog.set_resizable(False)
dialog.set_border_width(8)
label = Gtk.Label(label=_('Enter a new title for the Terminator window...'))
name = Gtk.Entry()
name.set_activates_default(True)
if window.title.text != self.vte.get_window_title():
name.set_text(self.get_toplevel().title.text)
dialog.vbox.pack_start(label, False, False, 6)
dialog.vbox.pack_start(name, False, False, 6)
dialog.show_all()
res = dialog.run()
if res == Gtk.ResponseType.ACCEPT:
if name.get_text():
window.title.force_title(None)
window.title.force_title(name.get_text())
else:
window.title.force_title(None)
dialog.destroy()
return
def key_edit_tab_title(self):
window = self.get_toplevel()
if not window.is_child_notebook():
return
notebook = window.get_children()[0]
n_page = notebook.get_current_page()
page = notebook.get_nth_page(n_page)
label = notebook.get_tab_label(page)
label.edit()
def key_edit_terminal_title(self):
self.titlebar.label.edit()
def key_layout_launcher(self):
LAYOUTLAUNCHER=LayoutLauncher()
def key_page_up(self):
self.scroll_by_page(-1)
def key_page_down(self):
self.scroll_by_page(1)
def key_page_up_half(self):
self.scroll_by_page(-0.5)
def key_page_down_half(self):
self.scroll_by_page(0.5)
def key_line_up(self):
self.scroll_by_line(-1)
def key_line_down(self):
self.scroll_by_line(1)
def key_help(self):
manual_index_page = manual_lookup()
if manual_index_page:
self.open_url(manual_index_page)
# End key events
GObject.type_register(Terminal)
# vim: set expandtab ts=4 sw=4:
terminator-1.91/terminatorlib/configobj/ 0000775 0001750 0001750 00000000000 13055372500 020743 5 ustar steve steve 0000000 0000000 terminator-1.91/terminatorlib/configobj/configobj.py 0000664 0001750 0001750 00000254307 13054612071 023267 0 ustar steve steve 0000000 0000000 # configobj.py
# A config file reader/writer that supports nested sections in config files.
# Copyright (C) 2005-2010 Michael Foord, Nicola Larosa
# E-mail: fuzzyman AT voidspace DOT org DOT uk
# nico AT tekNico DOT net
# ConfigObj 4
# http://www.voidspace.org.uk/python/configobj.html
# Released subject to the BSD License
# Please see http://www.voidspace.org.uk/python/license.shtml
# Scripts maintained at http://www.voidspace.org.uk/python/index.shtml
# For information about bugfixes, updates and support, please join the
# ConfigObj mailing list:
# http://lists.sourceforge.net/lists/listinfo/configobj-develop
# Comments, suggestions and bug reports welcome.
from __future__ import generators
import os
import re
import sys
from codecs import BOM_UTF8, BOM_UTF16, BOM_UTF16_BE, BOM_UTF16_LE
# imported lazily to avoid startup performance hit if it isn't used
compiler = None
# A dictionary mapping BOM to
# the encoding to decode with, and what to set the
# encoding attribute to.
BOMS = {
BOM_UTF8: ('utf_8', None),
BOM_UTF16_BE: ('utf16_be', 'utf_16'),
BOM_UTF16_LE: ('utf16_le', 'utf_16'),
BOM_UTF16: ('utf_16', 'utf_16'),
}
# All legal variants of the BOM codecs.
# TODO: the list of aliases is not meant to be exhaustive, is there a
# better way ?
BOM_LIST = {
'utf_16': 'utf_16',
'u16': 'utf_16',
'utf16': 'utf_16',
'utf-16': 'utf_16',
'utf16_be': 'utf16_be',
'utf_16_be': 'utf16_be',
'utf-16be': 'utf16_be',
'utf16_le': 'utf16_le',
'utf_16_le': 'utf16_le',
'utf-16le': 'utf16_le',
'utf_8': 'utf_8',
'u8': 'utf_8',
'utf': 'utf_8',
'utf8': 'utf_8',
'utf-8': 'utf_8',
}
# Map of encodings to the BOM to write.
BOM_SET = {
'utf_8': BOM_UTF8,
'utf_16': BOM_UTF16,
'utf16_be': BOM_UTF16_BE,
'utf16_le': BOM_UTF16_LE,
None: BOM_UTF8
}
def match_utf8(encoding):
return BOM_LIST.get(encoding.lower()) == 'utf_8'
# Quote strings used for writing values
squot = "'%s'"
dquot = '"%s"'
noquot = "%s"
wspace_plus = ' \r\n\v\t\'"'
tsquot = '"""%s"""'
tdquot = "'''%s'''"
# Sentinel for use in getattr calls to replace hasattr
MISSING = object()
__version__ = '4.7.2'
try:
any
except NameError:
def any(iterable):
for entry in iterable:
if entry:
return True
return False
__all__ = (
'__version__',
'DEFAULT_INDENT_TYPE',
'DEFAULT_INTERPOLATION',
'ConfigObjError',
'NestingError',
'ParseError',
'DuplicateError',
'ConfigspecError',
'ConfigObj',
'SimpleVal',
'InterpolationError',
'InterpolationLoopError',
'MissingInterpolationOption',
'RepeatSectionError',
'ReloadError',
'UnreprError',
'UnknownType',
'flatten_errors',
'get_extra_values'
)
DEFAULT_INTERPOLATION = 'configparser'
DEFAULT_INDENT_TYPE = ' '
MAX_INTERPOL_DEPTH = 10
OPTION_DEFAULTS = {
'interpolation': True,
'raise_errors': False,
'list_values': True,
'create_empty': False,
'file_error': False,
'configspec': None,
'stringify': True,
# option may be set to one of ('', ' ', '\t')
'indent_type': None,
'encoding': None,
'default_encoding': None,
'unrepr': False,
'write_empty_values': False,
}
def getObj(s):
global compiler
if compiler is None:
import compiler
s = "a=" + s
p = compiler.parse(s)
return p.getChildren()[1].getChildren()[0].getChildren()[1]
class UnknownType(Exception):
pass
class Builder(object):
def build(self, o):
m = getattr(self, 'build_' + o.__class__.__name__, None)
if m is None:
raise UnknownType(o.__class__.__name__)
return m(o)
def build_List(self, o):
return map(self.build, o.getChildren())
def build_Const(self, o):
return o.value
def build_Dict(self, o):
d = {}
i = iter(map(self.build, o.getChildren()))
for el in i:
d[el] = i.next()
return d
def build_Tuple(self, o):
return tuple(self.build_List(o))
def build_Name(self, o):
if o.name == 'None':
return None
if o.name == 'True':
return True
if o.name == 'False':
return False
# An undefined Name
raise UnknownType('Undefined Name')
def build_Add(self, o):
real, imag = map(self.build_Const, o.getChildren())
try:
real = float(real)
except TypeError:
raise UnknownType('Add')
if not isinstance(imag, complex) or imag.real != 0.0:
raise UnknownType('Add')
return real+imag
def build_Getattr(self, o):
parent = self.build(o.expr)
return getattr(parent, o.attrname)
def build_UnarySub(self, o):
return -self.build_Const(o.getChildren()[0])
def build_UnaryAdd(self, o):
return self.build_Const(o.getChildren()[0])
_builder = Builder()
def unrepr(s):
if not s:
return s
return _builder.build(getObj(s))
class ConfigObjError(SyntaxError):
"""
This is the base class for all errors that ConfigObj raises.
It is a subclass of SyntaxError.
"""
def __init__(self, message='', line_number=None, line=''):
self.line = line
self.line_number = line_number
SyntaxError.__init__(self, message)
class NestingError(ConfigObjError):
"""
This error indicates a level of nesting that doesn't match.
"""
class ParseError(ConfigObjError):
"""
This error indicates that a line is badly written.
It is neither a valid ``key = value`` line,
nor a valid section marker line.
"""
class ReloadError(IOError):
"""
A 'reload' operation failed.
This exception is a subclass of ``IOError``.
"""
def __init__(self):
IOError.__init__(self, 'reload failed, filename is not set.')
class DuplicateError(ConfigObjError):
"""
The keyword or section specified already exists.
"""
class ConfigspecError(ConfigObjError):
"""
An error occured whilst parsing a configspec.
"""
class InterpolationError(ConfigObjError):
"""Base class for the two interpolation errors."""
class InterpolationLoopError(InterpolationError):
"""Maximum interpolation depth exceeded in string interpolation."""
def __init__(self, option):
InterpolationError.__init__(
self,
'interpolation loop detected in value "%s".' % option)
class RepeatSectionError(ConfigObjError):
"""
This error indicates additional sections in a section with a
``__many__`` (repeated) section.
"""
class MissingInterpolationOption(InterpolationError):
"""A value specified for interpolation was missing."""
def __init__(self, option):
msg = 'missing option "%s" in interpolation.' % option
InterpolationError.__init__(self, msg)
class UnreprError(ConfigObjError):
"""An error parsing in unrepr mode."""
class InterpolationEngine(object):
"""
A helper class to help perform string interpolation.
This class is an abstract base class; its descendants perform
the actual work.
"""
# compiled regexp to use in self.interpolate()
_KEYCRE = re.compile(r"%\(([^)]*)\)s")
_cookie = '%'
def __init__(self, section):
# the Section instance that "owns" this engine
self.section = section
def interpolate(self, key, value):
# short-cut
if not self._cookie in value:
return value
def recursive_interpolate(key, value, section, backtrail):
"""The function that does the actual work.
``value``: the string we're trying to interpolate.
``section``: the section in which that string was found
``backtrail``: a dict to keep track of where we've been,
to detect and prevent infinite recursion loops
This is similar to a depth-first-search algorithm.
"""
# Have we been here already?
if (key, section.name) in backtrail:
# Yes - infinite loop detected
raise InterpolationLoopError(key)
# Place a marker on our backtrail so we won't come back here again
backtrail[(key, section.name)] = 1
# Now start the actual work
match = self._KEYCRE.search(value)
while match:
# The actual parsing of the match is implementation-dependent,
# so delegate to our helper function
k, v, s = self._parse_match(match)
if k is None:
# That's the signal that no further interpolation is needed
replacement = v
else:
# Further interpolation may be needed to obtain final value
replacement = recursive_interpolate(k, v, s, backtrail)
# Replace the matched string with its final value
start, end = match.span()
value = ''.join((value[:start], replacement, value[end:]))
new_search_start = start + len(replacement)
# Pick up the next interpolation key, if any, for next time
# through the while loop
match = self._KEYCRE.search(value, new_search_start)
# Now safe to come back here again; remove marker from backtrail
del backtrail[(key, section.name)]
return value
# Back in interpolate(), all we have to do is kick off the recursive
# function with appropriate starting values
value = recursive_interpolate(key, value, self.section, {})
return value
def _fetch(self, key):
"""Helper function to fetch values from owning section.
Returns a 2-tuple: the value, and the section where it was found.
"""
# switch off interpolation before we try and fetch anything !
save_interp = self.section.main.interpolation
self.section.main.interpolation = False
# Start at section that "owns" this InterpolationEngine
current_section = self.section
while True:
# try the current section first
val = current_section.get(key)
if val is not None and not isinstance(val, Section):
break
# try "DEFAULT" next
val = current_section.get('DEFAULT', {}).get(key)
if val is not None and not isinstance(val, Section):
break
# move up to parent and try again
# top-level's parent is itself
if current_section.parent is current_section:
# reached top level, time to give up
break
current_section = current_section.parent
# restore interpolation to previous value before returning
self.section.main.interpolation = save_interp
if val is None:
raise MissingInterpolationOption(key)
return val, current_section
def _parse_match(self, match):
"""Implementation-dependent helper function.
Will be passed a match object corresponding to the interpolation
key we just found (e.g., "%(foo)s" or "$foo"). Should look up that
key in the appropriate config file section (using the ``_fetch()``
helper function) and return a 3-tuple: (key, value, section)
``key`` is the name of the key we're looking for
``value`` is the value found for that key
``section`` is a reference to the section where it was found
``key`` and ``section`` should be None if no further
interpolation should be performed on the resulting value
(e.g., if we interpolated "$$" and returned "$").
"""
raise NotImplementedError()
class ConfigParserInterpolation(InterpolationEngine):
"""Behaves like ConfigParser."""
_cookie = '%'
_KEYCRE = re.compile(r"%\(([^)]*)\)s")
def _parse_match(self, match):
key = match.group(1)
value, section = self._fetch(key)
return key, value, section
class TemplateInterpolation(InterpolationEngine):
"""Behaves like string.Template."""
_cookie = '$'
_delimiter = '$'
_KEYCRE = re.compile(r"""
\$(?:
(?P\$) | # Two $ signs
(?P[_a-z][_a-z0-9]*) | # $name format
{(?P[^}]*)} # ${name} format
)
""", re.IGNORECASE | re.VERBOSE)
def _parse_match(self, match):
# Valid name (in or out of braces): fetch value from section
key = match.group('named') or match.group('braced')
if key is not None:
value, section = self._fetch(key)
return key, value, section
# Escaped delimiter (e.g., $$): return single delimiter
if match.group('escaped') is not None:
# Return None for key and section to indicate it's time to stop
return None, self._delimiter, None
# Anything else: ignore completely, just return it unchanged
return None, match.group(), None
interpolation_engines = {
'configparser': ConfigParserInterpolation,
'template': TemplateInterpolation,
}
def __newobj__(cls, *args):
# Hack for pickle
return cls.__new__(cls, *args)
class Section(dict):
"""
A dictionary-like object that represents a section in a config file.
It does string interpolation if the 'interpolation' attribute
of the 'main' object is set to True.
Interpolation is tried first from this object, then from the 'DEFAULT'
section of this object, next from the parent and its 'DEFAULT' section,
and so on until the main object is reached.
A Section will behave like an ordered dictionary - following the
order of the ``scalars`` and ``sections`` attributes.
You can use this to change the order of members.
Iteration follows the order: scalars, then sections.
"""
def __setstate__(self, state):
dict.update(self, state[0])
self.__dict__.update(state[1])
def __reduce__(self):
state = (dict(self), self.__dict__)
return (__newobj__, (self.__class__,), state)
def __init__(self, parent, depth, main, indict=None, name=None):
"""
* parent is the section above
* depth is the depth level of this section
* main is the main ConfigObj
* indict is a dictionary to initialise the section with
"""
if indict is None:
indict = {}
dict.__init__(self)
# used for nesting level *and* interpolation
self.parent = parent
# used for the interpolation attribute
self.main = main
# level of nesting depth of this Section
self.depth = depth
# purely for information
self.name = name
#
self._initialise()
# we do this explicitly so that __setitem__ is used properly
# (rather than just passing to ``dict.__init__``)
for entry, value in indict.iteritems():
self[entry] = value
def _initialise(self):
# the sequence of scalar values in this Section
self.scalars = []
# the sequence of sections in this Section
self.sections = []
# for comments :-)
self.comments = {}
self.inline_comments = {}
# the configspec
self.configspec = None
# for defaults
self.defaults = []
self.default_values = {}
self.extra_values = []
self._created = False
def _interpolate(self, key, value):
try:
# do we already have an interpolation engine?
engine = self._interpolation_engine
except AttributeError:
# not yet: first time running _interpolate(), so pick the engine
name = self.main.interpolation
if name == True: # note that "if name:" would be incorrect here
# backwards-compatibility: interpolation=True means use default
name = DEFAULT_INTERPOLATION
name = name.lower() # so that "Template", "template", etc. all work
class_ = interpolation_engines.get(name, None)
if class_ is None:
# invalid value for self.main.interpolation
self.main.interpolation = False
return value
else:
# save reference to engine so we don't have to do this again
engine = self._interpolation_engine = class_(self)
# let the engine do the actual work
return engine.interpolate(key, value)
def __getitem__(self, key):
"""Fetch the item and do string interpolation."""
val = dict.__getitem__(self, key)
if self.main.interpolation:
if isinstance(val, basestring):
return self._interpolate(key, val)
if isinstance(val, list):
def _check(entry):
if isinstance(entry, basestring):
return self._interpolate(key, entry)
return entry
new = [_check(entry) for entry in val]
if new != val:
return new
return val
def __setitem__(self, key, value, unrepr=False):
"""
Correctly set a value.
Making dictionary values Section instances.
(We have to special case 'Section' instances - which are also dicts)
Keys must be strings.
Values need only be strings (or lists of strings) if
``main.stringify`` is set.
``unrepr`` must be set when setting a value to a dictionary, without
creating a new sub-section.
"""
if not isinstance(key, basestring):
raise ValueError('The key "%s" is not a string.' % key)
# add the comment
if key not in self.comments:
self.comments[key] = []
self.inline_comments[key] = ''
# remove the entry from defaults
if key in self.defaults:
self.defaults.remove(key)
#
if isinstance(value, Section):
if key not in self:
self.sections.append(key)
dict.__setitem__(self, key, value)
elif isinstance(value, dict) and not unrepr:
# First create the new depth level,
# then create the section
if key not in self:
self.sections.append(key)
new_depth = self.depth + 1
dict.__setitem__(
self,
key,
Section(
self,
new_depth,
self.main,
indict=value,
name=key))
else:
if key not in self:
self.scalars.append(key)
if not self.main.stringify:
if isinstance(value, basestring):
pass
elif isinstance(value, (list, tuple)):
for entry in value:
if not isinstance(entry, basestring):
raise TypeError('Value is not a string "%s".' % entry)
else:
raise TypeError('Value is not a string "%s".' % value)
dict.__setitem__(self, key, value)
def __delitem__(self, key):
"""Remove items from the sequence when deleting."""
dict. __delitem__(self, key)
if key in self.scalars:
self.scalars.remove(key)
else:
self.sections.remove(key)
del self.comments[key]
del self.inline_comments[key]
def get(self, key, default=None):
"""A version of ``get`` that doesn't bypass string interpolation."""
try:
return self[key]
except KeyError:
return default
def update(self, indict):
"""
A version of update that uses our ``__setitem__``.
"""
for entry in indict:
self[entry] = indict[entry]
def pop(self, key, default=MISSING):
"""
'D.pop(k[,d]) -> v, remove specified key and return the corresponding value.
If key is not found, d is returned if given, otherwise KeyError is raised'
"""
try:
val = self[key]
except KeyError:
if default is MISSING:
raise
val = default
else:
del self[key]
return val
def popitem(self):
"""Pops the first (key,val)"""
sequence = (self.scalars + self.sections)
if not sequence:
raise KeyError(": 'popitem(): dictionary is empty'")
key = sequence[0]
val = self[key]
del self[key]
return key, val
def clear(self):
"""
A version of clear that also affects scalars/sections
Also clears comments and configspec.
Leaves other attributes alone :
depth/main/parent are not affected
"""
dict.clear(self)
self.scalars = []
self.sections = []
self.comments = {}
self.inline_comments = {}
self.configspec = None
self.defaults = []
self.extra_values = []
def setdefault(self, key, default=None):
"""A version of setdefault that sets sequence if appropriate."""
try:
return self[key]
except KeyError:
self[key] = default
return self[key]
def items(self):
"""D.items() -> list of D's (key, value) pairs, as 2-tuples"""
return zip((self.scalars + self.sections), self.values())
def keys(self):
"""D.keys() -> list of D's keys"""
return (self.scalars + self.sections)
def values(self):
"""D.values() -> list of D's values"""
return [self[key] for key in (self.scalars + self.sections)]
def iteritems(self):
"""D.iteritems() -> an iterator over the (key, value) items of D"""
return iter(self.items())
def iterkeys(self):
"""D.iterkeys() -> an iterator over the keys of D"""
return iter((self.scalars + self.sections))
__iter__ = iterkeys
def itervalues(self):
"""D.itervalues() -> an iterator over the values of D"""
return iter(self.values())
def __repr__(self):
"""x.__repr__() <==> repr(x)"""
def _getval(key):
try:
return self[key]
except MissingInterpolationOption:
return dict.__getitem__(self, key)
return '{%s}' % ', '.join([('%s: %s' % (repr(key), repr(_getval(key))))
for key in (self.scalars + self.sections)])
__str__ = __repr__
__str__.__doc__ = "x.__str__() <==> str(x)"
# Extra methods - not in a normal dictionary
def dict(self):
"""
Return a deepcopy of self as a dictionary.
All members that are ``Section`` instances are recursively turned to
ordinary dictionaries - by calling their ``dict`` method.
>>> n = a.dict()
>>> n == a
1
>>> n is a
0
"""
newdict = {}
for entry in self:
this_entry = self[entry]
if isinstance(this_entry, Section):
this_entry = this_entry.dict()
elif isinstance(this_entry, list):
# create a copy rather than a reference
this_entry = list(this_entry)
elif isinstance(this_entry, tuple):
# create a copy rather than a reference
this_entry = tuple(this_entry)
newdict[entry] = this_entry
return newdict
def merge(self, indict):
"""
A recursive update - useful for merging config files.
>>> a = '''[section1]
... option1 = True
... [[subsection]]
... more_options = False
... # end of file'''.splitlines()
>>> b = '''# File is user.ini
... [section1]
... option1 = False
... # end of file'''.splitlines()
>>> c1 = ConfigObj(b)
>>> c2 = ConfigObj(a)
>>> c2.merge(c1)
>>> c2
ConfigObj({'section1': {'option1': 'False', 'subsection': {'more_options': 'False'}}})
"""
for key, val in indict.items():
if (key in self and isinstance(self[key], dict) and
isinstance(val, dict)):
self[key].merge(val)
else:
self[key] = val
def rename(self, oldkey, newkey):
"""
Change a keyname to another, without changing position in sequence.
Implemented so that transformations can be made on keys,
as well as on values. (used by encode and decode)
Also renames comments.
"""
if oldkey in self.scalars:
the_list = self.scalars
elif oldkey in self.sections:
the_list = self.sections
else:
raise KeyError('Key "%s" not found.' % oldkey)
pos = the_list.index(oldkey)
#
val = self[oldkey]
dict.__delitem__(self, oldkey)
dict.__setitem__(self, newkey, val)
the_list.remove(oldkey)
the_list.insert(pos, newkey)
comm = self.comments[oldkey]
inline_comment = self.inline_comments[oldkey]
del self.comments[oldkey]
del self.inline_comments[oldkey]
self.comments[newkey] = comm
self.inline_comments[newkey] = inline_comment
def walk(self, function, raise_errors=True,
call_on_sections=False, **keywargs):
"""
Walk every member and call a function on the keyword and value.
Return a dictionary of the return values
If the function raises an exception, raise the errror
unless ``raise_errors=False``, in which case set the return value to
``False``.
Any unrecognised keyword arguments you pass to walk, will be pased on
to the function you pass in.
Note: if ``call_on_sections`` is ``True`` then - on encountering a
subsection, *first* the function is called for the *whole* subsection,
and then recurses into it's members. This means your function must be
able to handle strings, dictionaries and lists. This allows you
to change the key of subsections as well as for ordinary members. The
return value when called on the whole subsection has to be discarded.
See the encode and decode methods for examples, including functions.
.. admonition:: caution
You can use ``walk`` to transform the names of members of a section
but you mustn't add or delete members.
>>> config = '''[XXXXsection]
... XXXXkey = XXXXvalue'''.splitlines()
>>> cfg = ConfigObj(config)
>>> cfg
ConfigObj({'XXXXsection': {'XXXXkey': 'XXXXvalue'}})
>>> def transform(section, key):
... val = section[key]
... newkey = key.replace('XXXX', 'CLIENT1')
... section.rename(key, newkey)
... if isinstance(val, (tuple, list, dict)):
... pass
... else:
... val = val.replace('XXXX', 'CLIENT1')
... section[newkey] = val
>>> cfg.walk(transform, call_on_sections=True)
{'CLIENT1section': {'CLIENT1key': None}}
>>> cfg
ConfigObj({'CLIENT1section': {'CLIENT1key': 'CLIENT1value'}})
"""
out = {}
# scalars first
for i in range(len(self.scalars)):
entry = self.scalars[i]
try:
val = function(self, entry, **keywargs)
# bound again in case name has changed
entry = self.scalars[i]
out[entry] = val
except Exception:
if raise_errors:
raise
else:
entry = self.scalars[i]
out[entry] = False
# then sections
for i in range(len(self.sections)):
entry = self.sections[i]
if call_on_sections:
try:
function(self, entry, **keywargs)
except Exception:
if raise_errors:
raise
else:
entry = self.sections[i]
out[entry] = False
# bound again in case name has changed
entry = self.sections[i]
# previous result is discarded
out[entry] = self[entry].walk(
function,
raise_errors=raise_errors,
call_on_sections=call_on_sections,
**keywargs)
return out
def as_bool(self, key):
"""
Accepts a key as input. The corresponding value must be a string or
the objects (``True`` or 1) or (``False`` or 0). We allow 0 and 1 to
retain compatibility with Python 2.2.
If the string is one of ``True``, ``On``, ``Yes``, or ``1`` it returns
``True``.
If the string is one of ``False``, ``Off``, ``No``, or ``0`` it returns
``False``.
``as_bool`` is not case sensitive.
Any other input will raise a ``ValueError``.
>>> a = ConfigObj()
>>> a['a'] = 'fish'
>>> a.as_bool('a')
Traceback (most recent call last):
ValueError: Value "fish" is neither True nor False
>>> a['b'] = 'True'
>>> a.as_bool('b')
1
>>> a['b'] = 'off'
>>> a.as_bool('b')
0
"""
val = self[key]
if val == True:
return True
elif val == False:
return False
else:
try:
if not isinstance(val, basestring):
# TODO: Why do we raise a KeyError here?
raise KeyError()
else:
return self.main._bools[val.lower()]
except KeyError:
raise ValueError('Value "%s" is neither True nor False' % val)
def as_int(self, key):
"""
A convenience method which coerces the specified value to an integer.
If the value is an invalid literal for ``int``, a ``ValueError`` will
be raised.
>>> a = ConfigObj()
>>> a['a'] = 'fish'
>>> a.as_int('a')
Traceback (most recent call last):
ValueError: invalid literal for int() with base 10: 'fish'
>>> a['b'] = '1'
>>> a.as_int('b')
1
>>> a['b'] = '3.2'
>>> a.as_int('b')
Traceback (most recent call last):
ValueError: invalid literal for int() with base 10: '3.2'
"""
return int(self[key])
def as_float(self, key):
"""
A convenience method which coerces the specified value to a float.
If the value is an invalid literal for ``float``, a ``ValueError`` will
be raised.
>>> a = ConfigObj()
>>> a['a'] = 'fish'
>>> a.as_float('a')
Traceback (most recent call last):
ValueError: invalid literal for float(): fish
>>> a['b'] = '1'
>>> a.as_float('b')
1.0
>>> a['b'] = '3.2'
>>> a.as_float('b')
3.2000000000000002
"""
return float(self[key])
def as_list(self, key):
"""
A convenience method which fetches the specified value, guaranteeing
that it is a list.
>>> a = ConfigObj()
>>> a['a'] = 1
>>> a.as_list('a')
[1]
>>> a['a'] = (1,)
>>> a.as_list('a')
[1]
>>> a['a'] = [1]
>>> a.as_list('a')
[1]
"""
result = self[key]
if isinstance(result, (tuple, list)):
return list(result)
return [result]
def restore_default(self, key):
"""
Restore (and return) default value for the specified key.
This method will only work for a ConfigObj that was created
with a configspec and has been validated.
If there is no default value for this key, ``KeyError`` is raised.
"""
default = self.default_values[key]
dict.__setitem__(self, key, default)
if key not in self.defaults:
self.defaults.append(key)
return default
def restore_defaults(self):
"""
Recursively restore default values to all members
that have them.
This method will only work for a ConfigObj that was created
with a configspec and has been validated.
It doesn't delete or modify entries without default values.
"""
for key in self.default_values:
self.restore_default(key)
for section in self.sections:
self[section].restore_defaults()
class ConfigObj(Section):
"""An object to read, create, and write config files."""
_keyword = re.compile(r'''^ # line start
(\s*) # indentation
( # keyword
(?:".*?")| # double quotes
(?:'.*?')| # single quotes
(?:[^'"=].*?) # no quotes
)
\s*=\s* # divider
(.*) # value (including list values and comments)
$ # line end
''',
re.VERBOSE)
_sectionmarker = re.compile(r'''^
(\s*) # 1: indentation
((?:\[\s*)+) # 2: section marker open
( # 3: section name open
(?:"\s*\S.*?\s*")| # at least one non-space with double quotes
(?:'\s*\S.*?\s*')| # at least one non-space with single quotes
(?:[^'"\s].*?) # at least one non-space unquoted
) # section name close
((?:\s*\])+) # 4: section marker close
\s*(\#.*)? # 5: optional comment
$''',
re.VERBOSE)
# this regexp pulls list values out as a single string
# or single values and comments
# FIXME: this regex adds a '' to the end of comma terminated lists
# workaround in ``_handle_value``
_valueexp = re.compile(r'''^
(?:
(?:
(
(?:
(?:
(?:".*?")| # double quotes
(?:'.*?')| # single quotes
(?:[^'",\#][^,\#]*?) # unquoted
)
\s*,\s* # comma
)* # match all list items ending in a comma (if any)
)
(
(?:".*?")| # double quotes
(?:'.*?')| # single quotes
(?:[^'",\#\s][^,]*?)| # unquoted
(?:(? 1:
msg = "Parsing failed with several errors.\nFirst error %s" % info
error = ConfigObjError(msg)
else:
error = self._errors[0]
# set the errors attribute; it's a list of tuples:
# (error_type, message, line_number)
error.errors = self._errors
# set the config attribute
error.config = self
raise error
# delete private attributes
del self._errors
if configspec is None:
self.configspec = None
else:
self._handle_configspec(configspec)
def _initialise(self, options=None):
if options is None:
options = OPTION_DEFAULTS
# initialise a few variables
self.filename = None
self._errors = []
self.raise_errors = options['raise_errors']
self.interpolation = options['interpolation']
self.list_values = options['list_values']
self.create_empty = options['create_empty']
self.file_error = options['file_error']
self.stringify = options['stringify']
self.indent_type = options['indent_type']
self.encoding = options['encoding']
self.default_encoding = options['default_encoding']
self.BOM = False
self.newlines = None
self.write_empty_values = options['write_empty_values']
self.unrepr = options['unrepr']
self.initial_comment = []
self.final_comment = []
self.configspec = None
if self._inspec:
self.list_values = False
# Clear section attributes as well
Section._initialise(self)
def __repr__(self):
def _getval(key):
try:
return self[key]
except MissingInterpolationOption:
return dict.__getitem__(self, key)
return ('ConfigObj({%s})' %
', '.join([('%s: %s' % (repr(key), repr(_getval(key))))
for key in (self.scalars + self.sections)]))
def _handle_bom(self, infile):
"""
Handle any BOM, and decode if necessary.
If an encoding is specified, that *must* be used - but the BOM should
still be removed (and the BOM attribute set).
(If the encoding is wrongly specified, then a BOM for an alternative
encoding won't be discovered or removed.)
If an encoding is not specified, UTF8 or UTF16 BOM will be detected and
removed. The BOM attribute will be set. UTF16 will be decoded to
unicode.
NOTE: This method must not be called with an empty ``infile``.
Specifying the *wrong* encoding is likely to cause a
``UnicodeDecodeError``.
``infile`` must always be returned as a list of lines, but may be
passed in as a single string.
"""
if ((self.encoding is not None) and
(self.encoding.lower() not in BOM_LIST)):
# No need to check for a BOM
# the encoding specified doesn't have one
# just decode
return self._decode(infile, self.encoding)
if isinstance(infile, (list, tuple)):
line = infile[0]
else:
line = infile
if self.encoding is not None:
# encoding explicitly supplied
# And it could have an associated BOM
# TODO: if encoding is just UTF16 - we ought to check for both
# TODO: big endian and little endian versions.
enc = BOM_LIST[self.encoding.lower()]
if enc == 'utf_16':
# For UTF16 we try big endian and little endian
for BOM, (encoding, final_encoding) in BOMS.items():
if not final_encoding:
# skip UTF8
continue
if infile.startswith(BOM):
### BOM discovered
##self.BOM = True
# Don't need to remove BOM
return self._decode(infile, encoding)
# If we get this far, will *probably* raise a DecodeError
# As it doesn't appear to start with a BOM
return self._decode(infile, self.encoding)
# Must be UTF8
BOM = BOM_SET[enc]
if not line.startswith(BOM):
return self._decode(infile, self.encoding)
newline = line[len(BOM):]
# BOM removed
if isinstance(infile, (list, tuple)):
infile[0] = newline
else:
infile = newline
self.BOM = True
return self._decode(infile, self.encoding)
# No encoding specified - so we need to check for UTF8/UTF16
for BOM, (encoding, final_encoding) in BOMS.items():
if not line.startswith(BOM):
continue
else:
# BOM discovered
self.encoding = final_encoding
if not final_encoding:
self.BOM = True
# UTF8
# remove BOM
newline = line[len(BOM):]
if isinstance(infile, (list, tuple)):
infile[0] = newline
else:
infile = newline
# UTF8 - don't decode
if isinstance(infile, basestring):
return infile.splitlines(True)
else:
return infile
# UTF16 - have to decode
return self._decode(infile, encoding)
# No BOM discovered and no encoding specified, just return
if isinstance(infile, basestring):
# infile read from a file will be a single string
return infile.splitlines(True)
return infile
def _a_to_u(self, aString):
"""Decode ASCII strings to unicode if a self.encoding is specified."""
if self.encoding:
return aString.decode('ascii')
else:
return aString
def _decode(self, infile, encoding):
"""
Decode infile to unicode. Using the specified encoding.
if is a string, it also needs converting to a list.
"""
if isinstance(infile, basestring):
# can't be unicode
# NOTE: Could raise a ``UnicodeDecodeError``
return infile.decode(encoding).splitlines(True)
for i, line in enumerate(infile):
if not isinstance(line, unicode):
# NOTE: The isinstance test here handles mixed lists of unicode/string
# NOTE: But the decode will break on any non-string values
# NOTE: Or could raise a ``UnicodeDecodeError``
infile[i] = line.decode(encoding)
return infile
def _decode_element(self, line):
"""Decode element to unicode if necessary."""
if not self.encoding:
return line
if isinstance(line, str) and self.default_encoding:
return line.decode(self.default_encoding)
return line
def _str(self, value):
"""
Used by ``stringify`` within validate, to turn non-string values
into strings.
"""
if not isinstance(value, basestring):
return str(value)
else:
return value
def _parse(self, infile):
"""Actually parse the config file."""
temp_list_values = self.list_values
if self.unrepr:
self.list_values = False
comment_list = []
done_start = False
this_section = self
maxline = len(infile) - 1
cur_index = -1
reset_comment = False
while cur_index < maxline:
if reset_comment:
comment_list = []
cur_index += 1
line = infile[cur_index]
sline = line.strip()
# do we have anything on the line ?
if not sline or sline.startswith('#'):
reset_comment = False
comment_list.append(line)
continue
if not done_start:
# preserve initial comment
self.initial_comment = comment_list
comment_list = []
done_start = True
reset_comment = True
# first we check if it's a section marker
mat = self._sectionmarker.match(line)
if mat is not None:
# is a section line
(indent, sect_open, sect_name, sect_close, comment) = mat.groups()
if indent and (self.indent_type is None):
self.indent_type = indent
cur_depth = sect_open.count('[')
if cur_depth != sect_close.count(']'):
self._handle_error("Cannot compute the section depth at line %s.",
NestingError, infile, cur_index)
continue
if cur_depth < this_section.depth:
# the new section is dropping back to a previous level
try:
parent = self._match_depth(this_section,
cur_depth).parent
except SyntaxError:
self._handle_error("Cannot compute nesting level at line %s.",
NestingError, infile, cur_index)
continue
elif cur_depth == this_section.depth:
# the new section is a sibling of the current section
parent = this_section.parent
elif cur_depth == this_section.depth + 1:
# the new section is a child the current section
parent = this_section
else:
self._handle_error("Section too nested at line %s.",
NestingError, infile, cur_index)
sect_name = self._unquote(sect_name)
if sect_name in parent:
self._handle_error('Duplicate section name at line %s.',
DuplicateError, infile, cur_index)
continue
# create the new section
this_section = Section(
parent,
cur_depth,
self,
name=sect_name)
parent[sect_name] = this_section
parent.inline_comments[sect_name] = comment
parent.comments[sect_name] = comment_list
continue
#
# it's not a section marker,
# so it should be a valid ``key = value`` line
mat = self._keyword.match(line)
if mat is None:
# it neither matched as a keyword
# or a section marker
self._handle_error(
'Invalid line at line "%s".',
ParseError, infile, cur_index)
else:
# is a keyword value
# value will include any inline comment
(indent, key, value) = mat.groups()
if indent and (self.indent_type is None):
self.indent_type = indent
# check for a multiline value
if value[:3] in ['"""', "'''"]:
try:
value, comment, cur_index = self._multiline(
value, infile, cur_index, maxline)
except SyntaxError:
self._handle_error(
'Parse error in value at line %s.',
ParseError, infile, cur_index)
continue
else:
if self.unrepr:
comment = ''
try:
value = unrepr(value)
except Exception, e:
if type(e) == UnknownType:
msg = 'Unknown name or type in value at line %s.'
else:
msg = 'Parse error in value at line %s.'
self._handle_error(msg, UnreprError, infile,
cur_index)
continue
else:
if self.unrepr:
comment = ''
try:
value = unrepr(value)
except Exception, e:
if isinstance(e, UnknownType):
msg = 'Unknown name or type in value at line %s.'
else:
msg = 'Parse error in value at line %s.'
self._handle_error(msg, UnreprError, infile,
cur_index)
continue
else:
# extract comment and lists
try:
(value, comment) = self._handle_value(value)
except SyntaxError:
self._handle_error(
'Parse error in value at line %s.',
ParseError, infile, cur_index)
continue
#
key = self._unquote(key)
if key in this_section:
self._handle_error(
'Duplicate keyword name at line %s.',
DuplicateError, infile, cur_index)
continue
# add the key.
# we set unrepr because if we have got this far we will never
# be creating a new section
this_section.__setitem__(key, value, unrepr=True)
this_section.inline_comments[key] = comment
this_section.comments[key] = comment_list
continue
#
if self.indent_type is None:
# no indentation used, set the type accordingly
self.indent_type = ''
# preserve the final comment
if not self and not self.initial_comment:
self.initial_comment = comment_list
elif not reset_comment:
self.final_comment = comment_list
self.list_values = temp_list_values
def _match_depth(self, sect, depth):
"""
Given a section and a depth level, walk back through the sections
parents to see if the depth level matches a previous section.
Return a reference to the right section,
or raise a SyntaxError.
"""
while depth < sect.depth:
if sect is sect.parent:
# we've reached the top level already
raise SyntaxError()
sect = sect.parent
if sect.depth == depth:
return sect
# shouldn't get here
raise SyntaxError()
def _handle_error(self, text, ErrorClass, infile, cur_index):
"""
Handle an error according to the error settings.
Either raise the error or store it.
The error will have occured at ``cur_index``
"""
line = infile[cur_index]
cur_index += 1
message = text % cur_index
error = ErrorClass(message, cur_index, line)
if self.raise_errors:
# raise the error - parsing stops here
raise error
# store the error
# reraise when parsing has finished
self._errors.append(error)
def _unquote(self, value):
"""Return an unquoted version of a value"""
if not value:
# should only happen during parsing of lists
raise SyntaxError
if (value[0] == value[-1]) and (value[0] in ('"', "'")):
value = value[1:-1]
return value
def _quote(self, value, multiline=True):
"""
Return a safely quoted version of a value.
Raise a ConfigObjError if the value cannot be safely quoted.
If multiline is ``True`` (default) then use triple quotes
if necessary.
* Don't quote values that don't need it.
* Recursively quote members of a list and return a comma joined list.
* Multiline is ``False`` for lists.
* Obey list syntax for empty and single member lists.
If ``list_values=False`` then the value is only quoted if it contains
a ``\\n`` (is multiline) or '#'.
If ``write_empty_values`` is set, and the value is an empty string, it
won't be quoted.
"""
if multiline and self.write_empty_values and value == '':
# Only if multiline is set, so that it is used for values not
# keys, and not values that are part of a list
return ''
if multiline and isinstance(value, (list, tuple)):
if not value:
return ','
elif len(value) == 1:
return self._quote(value[0], multiline=False) + ','
return ', '.join([self._quote(val, multiline=False)
for val in value])
if not isinstance(value, basestring):
if self.stringify:
value = str(value)
else:
raise TypeError('Value "%s" is not a string.' % value)
if not value:
return '""'
no_lists_no_quotes = not self.list_values and '\n' not in value and '#' not in value
need_triple = multiline and ((("'" in value) and ('"' in value)) or ('\n' in value ))
hash_triple_quote = multiline and not need_triple and ("'" in value) and ('"' in value) and ('#' in value)
check_for_single = (no_lists_no_quotes or not need_triple) and not hash_triple_quote
if check_for_single:
if not self.list_values:
# we don't quote if ``list_values=False``
quot = noquot
# for normal values either single or double quotes will do
elif '\n' in value:
# will only happen if multiline is off - e.g. '\n' in key
raise ConfigObjError('Value "%s" cannot be safely quoted.' % value)
elif ((value[0] not in wspace_plus) and
(value[-1] not in wspace_plus) and
(',' not in value)):
quot = noquot
else:
quot = self._get_single_quote(value)
else:
# if value has '\n' or "'" *and* '"', it will need triple quotes
quot = self._get_triple_quote(value)
if quot == noquot and '#' in value and self.list_values:
quot = self._get_single_quote(value)
return quot % value
def _get_single_quote(self, value):
if ("'" in value) and ('"' in value):
raise ConfigObjError('Value "%s" cannot be safely quoted.' % value)
elif '"' in value:
quot = squot
else:
quot = dquot
return quot
def _get_triple_quote(self, value):
if (value.find('"""') != -1) and (value.find("'''") != -1):
raise ConfigObjError('Value "%s" cannot be safely quoted.' % value)
if value.find('"""') == -1:
quot = tdquot
else:
quot = tsquot
return quot
def _handle_value(self, value):
"""
Given a value string, unquote, remove comment,
handle lists. (including empty and single member lists)
"""
if self._inspec:
# Parsing a configspec so don't handle comments
return (value, '')
# do we look for lists in values ?
if not self.list_values:
mat = self._nolistvalue.match(value)
if mat is None:
raise SyntaxError()
# NOTE: we don't unquote here
return mat.groups()
#
mat = self._valueexp.match(value)
if mat is None:
# the value is badly constructed, probably badly quoted,
# or an invalid list
raise SyntaxError()
(list_values, single, empty_list, comment) = mat.groups()
if (list_values == '') and (single is None):
# change this if you want to accept empty values
raise SyntaxError()
# NOTE: note there is no error handling from here if the regex
# is wrong: then incorrect values will slip through
if empty_list is not None:
# the single comma - meaning an empty list
return ([], comment)
if single is not None:
# handle empty values
if list_values and not single:
# FIXME: the '' is a workaround because our regex now matches
# '' at the end of a list if it has a trailing comma
single = None
else:
single = single or '""'
single = self._unquote(single)
if list_values == '':
# not a list value
return (single, comment)
the_list = self._listvalueexp.findall(list_values)
the_list = [self._unquote(val) for val in the_list]
if single is not None:
the_list += [single]
return (the_list, comment)
def _multiline(self, value, infile, cur_index, maxline):
"""Extract the value, where we are in a multiline situation."""
quot = value[:3]
newvalue = value[3:]
single_line = self._triple_quote[quot][0]
multi_line = self._triple_quote[quot][1]
mat = single_line.match(value)
if mat is not None:
retval = list(mat.groups())
retval.append(cur_index)
return retval
elif newvalue.find(quot) != -1:
# somehow the triple quote is missing
raise SyntaxError()
#
while cur_index < maxline:
cur_index += 1
newvalue += '\n'
line = infile[cur_index]
if line.find(quot) == -1:
newvalue += line
else:
# end of multiline, process it
break
else:
# we've got to the end of the config, oops...
raise SyntaxError()
mat = multi_line.match(line)
if mat is None:
# a badly formed line
raise SyntaxError()
(value, comment) = mat.groups()
return (newvalue + value, comment, cur_index)
def _handle_configspec(self, configspec):
"""Parse the configspec."""
# FIXME: Should we check that the configspec was created with the
# correct settings ? (i.e. ``list_values=False``)
if not isinstance(configspec, ConfigObj):
try:
configspec = ConfigObj(configspec,
raise_errors=True,
file_error=True,
_inspec=True)
except ConfigObjError, e:
# FIXME: Should these errors have a reference
# to the already parsed ConfigObj ?
raise ConfigspecError('Parsing configspec failed: %s' % e)
except IOError, e:
raise IOError('Reading configspec failed: %s' % e)
self.configspec = configspec
def _set_configspec(self, section, copy):
"""
Called by validate. Handles setting the configspec on subsections
including sections to be validated by __many__
"""
configspec = section.configspec
many = configspec.get('__many__')
if isinstance(many, dict):
for entry in section.sections:
if entry not in configspec:
section[entry].configspec = many
for entry in configspec.sections:
if entry == '__many__':
continue
if entry not in section:
section[entry] = {}
section[entry]._created = True
if copy:
# copy comments
section.comments[entry] = configspec.comments.get(entry, [])
section.inline_comments[entry] = configspec.inline_comments.get(entry, '')
# Could be a scalar when we expect a section
if isinstance(section[entry], Section):
section[entry].configspec = configspec[entry]
def _write_line(self, indent_string, entry, this_entry, comment):
"""Write an individual line, for the write method"""
# NOTE: the calls to self._quote here handles non-StringType values.
if not self.unrepr:
val = self._decode_element(self._quote(this_entry))
else:
val = repr(this_entry)
return '%s%s%s%s%s' % (indent_string,
self._decode_element(self._quote(entry, multiline=False)),
self._a_to_u(' = '),
val,
self._decode_element(comment))
def _write_marker(self, indent_string, depth, entry, comment):
"""Write a section marker line"""
return '%s%s%s%s%s' % (indent_string,
self._a_to_u('[' * depth),
self._quote(self._decode_element(entry), multiline=False),
self._a_to_u(']' * depth),
self._decode_element(comment))
def _handle_comment(self, comment):
"""Deal with a comment."""
if not comment:
return ''
start = self.indent_type
if not comment.startswith('#'):
start += self._a_to_u(' # ')
return (start + comment)
# Public methods
def write(self, outfile=None, section=None):
"""
Write the current ConfigObj as a file
tekNico: FIXME: use StringIO instead of real files
>>> filename = a.filename
>>> a.filename = 'test.ini'
>>> a.write()
>>> a.filename = filename
>>> a == ConfigObj('test.ini', raise_errors=True)
1
>>> import os
>>> os.remove('test.ini')
"""
if self.indent_type is None:
# this can be true if initialised from a dictionary
self.indent_type = DEFAULT_INDENT_TYPE
out = []
cs = self._a_to_u('#')
csp = self._a_to_u('# ')
if section is None:
int_val = self.interpolation
self.interpolation = False
section = self
for line in self.initial_comment:
line = self._decode_element(line)
stripped_line = line.strip()
if stripped_line and not stripped_line.startswith(cs):
line = csp + line
out.append(line)
indent_string = self.indent_type * section.depth
# Do a little sorting for convenience
section.scalars = sorted(section.scalars)
section.sections = sorted(section.sections)
if 'default' in section.scalars:
# pop it and move to front
section.scalars.remove('default')
section.scalars.insert(0, 'default')
if 'default' in section.sections:
section.sections.remove('default')
section.sections.insert(0, 'default')
for entry in (section.scalars + section.sections):
if entry in section.defaults:
# don't write out default values
continue
for comment_line in section.comments[entry]:
comment_line = self._decode_element(comment_line.lstrip())
if comment_line and not comment_line.startswith(cs):
comment_line = csp + comment_line
out.append(indent_string + comment_line)
this_entry = section[entry]
comment = self._handle_comment(section.inline_comments[entry])
if isinstance(this_entry, dict):
# a section
out.append(self._write_marker(
indent_string,
this_entry.depth,
entry,
comment))
out.extend(self.write(section=this_entry))
else:
out.append(self._write_line(
indent_string,
entry,
this_entry,
comment))
if section is self:
for line in self.final_comment:
line = self._decode_element(line)
stripped_line = line.strip()
if stripped_line and not stripped_line.startswith(cs):
line = csp + line
out.append(line)
self.interpolation = int_val
if section is not self:
return out
if (self.filename is None) and (outfile is None):
# output a list of lines
# might need to encode
# NOTE: This will *screw* UTF16, each line will start with the BOM
if self.encoding:
out = [l.encode(self.encoding) for l in out]
if (self.BOM and ((self.encoding is None) or
(BOM_LIST.get(self.encoding.lower()) == 'utf_8'))):
# Add the UTF8 BOM
if not out:
out.append('')
out[0] = BOM_UTF8 + out[0]
return out
# Turn the list to a string, joined with correct newlines
newline = self.newlines or os.linesep
if (getattr(outfile, 'mode', None) is not None and outfile.mode == 'w'
and sys.platform == 'win32' and newline == '\r\n'):
# Windows specific hack to avoid writing '\r\r\n'
newline = '\n'
output = self._a_to_u(newline).join(out)
if self.encoding:
output = output.encode(self.encoding)
if self.BOM and ((self.encoding is None) or match_utf8(self.encoding)):
# Add the UTF8 BOM
output = BOM_UTF8 + output
if not output.endswith(newline):
output += newline
if outfile is not None:
outfile.write(output)
else:
h = open(self.filename, 'wb')
h.write(output)
h.close()
def validate(self, validator, preserve_errors=False, copy=False,
section=None):
"""
Test the ConfigObj against a configspec.
It uses the ``validator`` object from *validate.py*.
To run ``validate`` on the current ConfigObj, call: ::
test = config.validate(validator)
(Normally having previously passed in the configspec when the ConfigObj
was created - you can dynamically assign a dictionary of checks to the
``configspec`` attribute of a section though).
It returns ``True`` if everything passes, or a dictionary of
pass/fails (True/False). If every member of a subsection passes, it
will just have the value ``True``. (It also returns ``False`` if all
members fail).
In addition, it converts the values from strings to their native
types if their checks pass (and ``stringify`` is set).
If ``preserve_errors`` is ``True`` (``False`` is default) then instead
of a marking a fail with a ``False``, it will preserve the actual
exception object. This can contain info about the reason for failure.
For example the ``VdtValueTooSmallError`` indicates that the value
supplied was too small. If a value (or section) is missing it will
still be marked as ``False``.
You must have the validate module to use ``preserve_errors=True``.
You can then use the ``flatten_errors`` function to turn your nested
results dictionary into a flattened list of failures - useful for
displaying meaningful error messages.
"""
if section is None:
if self.configspec is None:
raise ValueError('No configspec supplied.')
if preserve_errors:
# We do this once to remove a top level dependency on the validate module
# Which makes importing configobj faster
from validate import VdtMissingValue
self._vdtMissingValue = VdtMissingValue
section = self
if copy:
section.initial_comment = section.configspec.initial_comment
section.final_comment = section.configspec.final_comment
section.encoding = section.configspec.encoding
section.BOM = section.configspec.BOM
section.newlines = section.configspec.newlines
section.indent_type = section.configspec.indent_type
#
# section.default_values.clear() #??
configspec = section.configspec
self._set_configspec(section, copy)
def validate_entry(entry, spec, val, missing, ret_true, ret_false):
section.default_values.pop(entry, None)
try:
section.default_values[entry] = validator.get_default_value(configspec[entry])
except (KeyError, AttributeError, validator.baseErrorClass):
# No default, bad default or validator has no 'get_default_value'
# (e.g. SimpleVal)
pass
try:
check = validator.check(spec,
val,
missing=missing
)
except validator.baseErrorClass, e:
if not preserve_errors or isinstance(e, self._vdtMissingValue):
out[entry] = False
else:
# preserve the error
out[entry] = e
ret_false = False
ret_true = False
else:
ret_false = False
out[entry] = True
if self.stringify or missing:
# if we are doing type conversion
# or the value is a supplied default
if not self.stringify:
if isinstance(check, (list, tuple)):
# preserve lists
check = [self._str(item) for item in check]
elif missing and check is None:
# convert the None from a default to a ''
check = ''
else:
check = self._str(check)
if (check != val) or missing:
section[entry] = check
if not copy and missing and entry not in section.defaults:
section.defaults.append(entry)
return ret_true, ret_false
#
out = {}
ret_true = True
ret_false = True
unvalidated = [k for k in section.scalars if k not in configspec]
incorrect_sections = [k for k in configspec.sections if k in section.scalars]
incorrect_scalars = [k for k in configspec.scalars if k in section.sections]
for entry in configspec.scalars:
if entry in ('__many__', '___many___'):
# reserved names
continue
if (not entry in section.scalars) or (entry in section.defaults):
# missing entries
# or entries from defaults
missing = True
val = None
if copy and entry not in section.scalars:
# copy comments
section.comments[entry] = (
configspec.comments.get(entry, []))
section.inline_comments[entry] = (
configspec.inline_comments.get(entry, ''))
#
else:
missing = False
val = section[entry]
ret_true, ret_false = validate_entry(entry, configspec[entry], val,
missing, ret_true, ret_false)
many = None
if '__many__' in configspec.scalars:
many = configspec['__many__']
elif '___many___' in configspec.scalars:
many = configspec['___many___']
if many is not None:
for entry in unvalidated:
val = section[entry]
ret_true, ret_false = validate_entry(entry, many, val, False,
ret_true, ret_false)
unvalidated = []
for entry in incorrect_scalars:
ret_true = False
if not preserve_errors:
out[entry] = False
else:
ret_false = False
msg = 'Value %r was provided as a section' % entry
out[entry] = validator.baseErrorClass(msg)
for entry in incorrect_sections:
ret_true = False
if not preserve_errors:
out[entry] = False
else:
ret_false = False
msg = 'Section %r was provided as a single value' % entry
out[entry] = validator.baseErrorClass(msg)
# Missing sections will have been created as empty ones when the
# configspec was read.
for entry in section.sections:
# FIXME: this means DEFAULT is not copied in copy mode
if section is self and entry == 'DEFAULT':
continue
if section[entry].configspec is None:
unvalidated.append(entry)
continue
if copy:
section.comments[entry] = configspec.comments.get(entry, [])
section.inline_comments[entry] = configspec.inline_comments.get(entry, '')
check = self.validate(validator, preserve_errors=preserve_errors, copy=copy, section=section[entry])
out[entry] = check
if check == False:
ret_true = False
elif check == True:
ret_false = False
else:
ret_true = False
section.extra_values = unvalidated
if preserve_errors and not section._created:
# If the section wasn't created (i.e. it wasn't missing)
# then we can't return False, we need to preserve errors
ret_false = False
#
if ret_false and preserve_errors and out:
# If we are preserving errors, but all
# the failures are from missing sections / values
# then we can return False. Otherwise there is a
# real failure that we need to preserve.
ret_false = not any(out.values())
if ret_true:
return True
elif ret_false:
return False
return out
def reset(self):
"""Clear ConfigObj instance and restore to 'freshly created' state."""
self.clear()
self._initialise()
# FIXME: Should be done by '_initialise', but ConfigObj constructor (and reload)
# requires an empty dictionary
self.configspec = None
# Just to be sure ;-)
self._original_configspec = None
def reload(self):
"""
Reload a ConfigObj from file.
This method raises a ``ReloadError`` if the ConfigObj doesn't have
a filename attribute pointing to a file.
"""
if not isinstance(self.filename, basestring):
raise ReloadError()
filename = self.filename
current_options = {}
for entry in OPTION_DEFAULTS:
if entry == 'configspec':
continue
current_options[entry] = getattr(self, entry)
configspec = self._original_configspec
current_options['configspec'] = configspec
self.clear()
self._initialise(current_options)
self._load(filename, configspec)
class SimpleVal(object):
"""
A simple validator.
Can be used to check that all members expected are present.
To use it, provide a configspec with all your members in (the value given
will be ignored). Pass an instance of ``SimpleVal`` to the ``validate``
method of your ``ConfigObj``. ``validate`` will return ``True`` if all
members are present, or a dictionary with True/False meaning
present/missing. (Whole missing sections will be replaced with ``False``)
"""
def __init__(self):
self.baseErrorClass = ConfigObjError
def check(self, check, member, missing=False):
"""A dummy check method, always returns the value unchanged."""
if missing:
raise self.baseErrorClass()
return member
def flatten_errors(cfg, res, levels=None, results=None):
"""
An example function that will turn a nested dictionary of results
(as returned by ``ConfigObj.validate``) into a flat list.
``cfg`` is the ConfigObj instance being checked, ``res`` is the results
dictionary returned by ``validate``.
(This is a recursive function, so you shouldn't use the ``levels`` or
``results`` arguments - they are used by the function.)
Returns a list of keys that failed. Each member of the list is a tuple::
([list of sections...], key, result)
If ``validate`` was called with ``preserve_errors=False`` (the default)
then ``result`` will always be ``False``.
*list of sections* is a flattened list of sections that the key was found
in.
If the section was missing (or a section was expected and a scalar provided
- or vice-versa) then key will be ``None``.
If the value (or section) was missing then ``result`` will be ``False``.
If ``validate`` was called with ``preserve_errors=True`` and a value
was present, but failed the check, then ``result`` will be the exception
object returned. You can use this as a string that describes the failure.
For example *The value "3" is of the wrong type*.
"""
if levels is None:
# first time called
levels = []
results = []
if res == True:
return results
if res == False or isinstance(res, Exception):
results.append((levels[:], None, res))
if levels:
levels.pop()
return results
for (key, val) in res.items():
if val == True:
continue
if isinstance(cfg.get(key), dict):
# Go down one level
levels.append(key)
flatten_errors(cfg[key], val, levels, results)
continue
results.append((levels[:], key, val))
#
# Go up one level
if levels:
levels.pop()
#
return results
def get_extra_values(conf, _prepend=()):
"""
Find all the values and sections not in the configspec from a validated
ConfigObj.
``get_extra_values`` returns a list of tuples where each tuple represents
either an extra section, or an extra value.
The tuples contain two values, a tuple representing the section the value
is in and the name of the extra values. For extra values in the top level
section the first member will be an empty tuple. For values in the 'foo'
section the first member will be ``('foo',)``. For members in the 'bar'
subsection of the 'foo' section the first member will be ``('foo', 'bar')``.
NOTE: If you call ``get_extra_values`` on a ConfigObj instance that hasn't
been validated it will return an empty list.
"""
out = []
out.extend([(_prepend, name) for name in conf.extra_values])
for name in conf.sections:
if name not in conf.extra_values:
out.extend(get_extra_values(conf[name], _prepend + (name,)))
return out
"""*A programming language is a medium of expression.* - Paul Graham"""
terminator-1.91/terminatorlib/configobj/__init__.py 0000664 0001750 0001750 00000000000 13054612071 023041 0 ustar steve steve 0000000 0000000 terminator-1.91/terminatorlib/configobj/validate.py 0000664 0001750 0001750 00000133101 13054612071 023104 0 ustar steve steve 0000000 0000000 # validate.py
# A Validator object
# Copyright (C) 2005-2010 Michael Foord, Mark Andrews, Nicola Larosa
# E-mail: fuzzyman AT voidspace DOT org DOT uk
# mark AT la-la DOT com
# nico AT tekNico DOT net
# This software is licensed under the terms of the BSD license.
# http://www.voidspace.org.uk/python/license.shtml
# Basically you're free to copy, modify, distribute and relicense it,
# So long as you keep a copy of the license with it.
# Scripts maintained at http://www.voidspace.org.uk/python/index.shtml
# For information about bugfixes, updates and support, please join the
# ConfigObj mailing list:
# http://lists.sourceforge.net/lists/listinfo/configobj-develop
# Comments, suggestions and bug reports welcome.
"""
The Validator object is used to check that supplied values
conform to a specification.
The value can be supplied as a string - e.g. from a config file.
In this case the check will also *convert* the value to
the required type. This allows you to add validation
as a transparent layer to access data stored as strings.
The validation checks that the data is correct *and*
converts it to the expected type.
Some standard checks are provided for basic data types.
Additional checks are easy to write. They can be
provided when the ``Validator`` is instantiated or
added afterwards.
The standard functions work with the following basic data types :
* integers
* floats
* booleans
* strings
* ip_addr
plus lists of these datatypes
Adding additional checks is done through coding simple functions.
The full set of standard checks are :
* 'integer': matches integer values (including negative)
Takes optional 'min' and 'max' arguments : ::
integer()
integer(3, 9) # any value from 3 to 9
integer(min=0) # any positive value
integer(max=9)
* 'float': matches float values
Has the same parameters as the integer check.
* 'boolean': matches boolean values - ``True`` or ``False``
Acceptable string values for True are :
true, on, yes, 1
Acceptable string values for False are :
false, off, no, 0
Any other value raises an error.
* 'ip_addr': matches an Internet Protocol address, v.4, represented
by a dotted-quad string, i.e. '1.2.3.4'.
* 'string': matches any string.
Takes optional keyword args 'min' and 'max'
to specify min and max lengths of the string.
* 'list': matches any list.
Takes optional keyword args 'min', and 'max' to specify min and
max sizes of the list. (Always returns a list.)
* 'tuple': matches any tuple.
Takes optional keyword args 'min', and 'max' to specify min and
max sizes of the tuple. (Always returns a tuple.)
* 'int_list': Matches a list of integers.
Takes the same arguments as list.
* 'float_list': Matches a list of floats.
Takes the same arguments as list.
* 'bool_list': Matches a list of boolean values.
Takes the same arguments as list.
* 'ip_addr_list': Matches a list of IP addresses.
Takes the same arguments as list.
* 'string_list': Matches a list of strings.
Takes the same arguments as list.
* 'mixed_list': Matches a list with different types in
specific positions. List size must match
the number of arguments.
Each position can be one of :
'integer', 'float', 'ip_addr', 'string', 'boolean'
So to specify a list with two strings followed
by two integers, you write the check as : ::
mixed_list('string', 'string', 'integer', 'integer')
* 'pass': This check matches everything ! It never fails
and the value is unchanged.
It is also the default if no check is specified.
* 'option': This check matches any from a list of options.
You specify this check with : ::
option('option 1', 'option 2', 'option 3')
You can supply a default value (returned if no value is supplied)
using the default keyword argument.
You specify a list argument for default using a list constructor syntax in
the check : ::
checkname(arg1, arg2, default=list('val 1', 'val 2', 'val 3'))
A badly formatted set of arguments will raise a ``VdtParamError``.
"""
__version__ = '1.0.1'
__all__ = (
'__version__',
'dottedQuadToNum',
'numToDottedQuad',
'ValidateError',
'VdtUnknownCheckError',
'VdtParamError',
'VdtTypeError',
'VdtValueError',
'VdtValueTooSmallError',
'VdtValueTooBigError',
'VdtValueTooShortError',
'VdtValueTooLongError',
'VdtMissingValue',
'Validator',
'is_integer',
'is_float',
'is_boolean',
'is_list',
'is_tuple',
'is_ip_addr',
'is_string',
'is_int_list',
'is_bool_list',
'is_float_list',
'is_string_list',
'is_ip_addr_list',
'is_mixed_list',
'is_option',
)
import re
_list_arg = re.compile(r'''
(?:
([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*list\(
(
(?:
\s*
(?:
(?:".*?")| # double quotes
(?:'.*?')| # single quotes
(?:[^'",\s\)][^,\)]*?) # unquoted
)
\s*,\s*
)*
(?:
(?:".*?")| # double quotes
(?:'.*?')| # single quotes
(?:[^'",\s\)][^,\)]*?) # unquoted
)? # last one
)
\)
)
''', re.VERBOSE | re.DOTALL) # two groups
_list_members = re.compile(r'''
(
(?:".*?")| # double quotes
(?:'.*?')| # single quotes
(?:[^'",\s=][^,=]*?) # unquoted
)
(?:
(?:\s*,\s*)|(?:\s*$) # comma
)
''', re.VERBOSE | re.DOTALL) # one group
_paramstring = r'''
(?:
(
(?:
[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*list\(
(?:
\s*
(?:
(?:".*?")| # double quotes
(?:'.*?')| # single quotes
(?:[^'",\s\)][^,\)]*?) # unquoted
)
\s*,\s*
)*
(?:
(?:".*?")| # double quotes
(?:'.*?')| # single quotes
(?:[^'",\s\)][^,\)]*?) # unquoted
)? # last one
\)
)|
(?:
(?:".*?")| # double quotes
(?:'.*?')| # single quotes
(?:[^'",\s=][^,=]*?)| # unquoted
(?: # keyword argument
[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*
(?:
(?:".*?")| # double quotes
(?:'.*?')| # single quotes
(?:[^'",\s=][^,=]*?) # unquoted
)
)
)
)
(?:
(?:\s*,\s*)|(?:\s*$) # comma
)
)
'''
_matchstring = '^%s*' % _paramstring
# Python pre 2.2.1 doesn't have bool
try:
bool
except NameError:
def bool(val):
"""Simple boolean equivalent function. """
if val:
return 1
else:
return 0
def dottedQuadToNum(ip):
"""
Convert decimal dotted quad string to long integer
>>> int(dottedQuadToNum('1 '))
1
>>> int(dottedQuadToNum(' 1.2'))
16777218
>>> int(dottedQuadToNum(' 1.2.3 '))
16908291
>>> int(dottedQuadToNum('1.2.3.4'))
16909060
>>> dottedQuadToNum('1.2.3. 4')
16909060
>>> dottedQuadToNum('255.255.255.255')
4294967295L
>>> dottedQuadToNum('255.255.255.256')
Traceback (most recent call last):
ValueError: Not a good dotted-quad IP: 255.255.255.256
"""
# import here to avoid it when ip_addr values are not used
import socket, struct
try:
return struct.unpack('!L',
socket.inet_aton(ip.strip()))[0]
except socket.error:
# bug in inet_aton, corrected in Python 2.3
if ip.strip() == '255.255.255.255':
return 0xFFFFFFFFL
else:
raise ValueError('Not a good dotted-quad IP: %s' % ip)
return
def numToDottedQuad(num):
"""
Convert long int to dotted quad string
>>> numToDottedQuad(-1L)
Traceback (most recent call last):
ValueError: Not a good numeric IP: -1
>>> numToDottedQuad(1L)
'0.0.0.1'
>>> numToDottedQuad(16777218L)
'1.0.0.2'
>>> numToDottedQuad(16908291L)
'1.2.0.3'
>>> numToDottedQuad(16909060L)
'1.2.3.4'
>>> numToDottedQuad(4294967295L)
'255.255.255.255'
>>> numToDottedQuad(4294967296L)
Traceback (most recent call last):
ValueError: Not a good numeric IP: 4294967296
"""
# import here to avoid it when ip_addr values are not used
import socket, struct
# no need to intercept here, 4294967295L is fine
if num > 4294967295L or num < 0:
raise ValueError('Not a good numeric IP: %s' % num)
try:
return socket.inet_ntoa(
struct.pack('!L', long(num)))
except (socket.error, struct.error, OverflowError):
raise ValueError('Not a good numeric IP: %s' % num)
class ValidateError(Exception):
"""
This error indicates that the check failed.
It can be the base class for more specific errors.
Any check function that fails ought to raise this error.
(or a subclass)
>>> raise ValidateError
Traceback (most recent call last):
ValidateError
"""
class VdtMissingValue(ValidateError):
"""No value was supplied to a check that needed one."""
class VdtUnknownCheckError(ValidateError):
"""An unknown check function was requested"""
def __init__(self, value):
"""
>>> raise VdtUnknownCheckError('yoda')
Traceback (most recent call last):
VdtUnknownCheckError: the check "yoda" is unknown.
"""
ValidateError.__init__(self, 'the check "%s" is unknown.' % (value,))
class VdtParamError(SyntaxError):
"""An incorrect parameter was passed"""
def __init__(self, name, value):
"""
>>> raise VdtParamError('yoda', 'jedi')
Traceback (most recent call last):
VdtParamError: passed an incorrect value "jedi" for parameter "yoda".
"""
SyntaxError.__init__(self, 'passed an incorrect value "%s" for parameter "%s".' % (value, name))
class VdtTypeError(ValidateError):
"""The value supplied was of the wrong type"""
def __init__(self, value):
"""
>>> raise VdtTypeError('jedi')
Traceback (most recent call last):
VdtTypeError: the value "jedi" is of the wrong type.
"""
ValidateError.__init__(self, 'the value "%s" is of the wrong type.' % (value,))
class VdtValueError(ValidateError):
"""The value supplied was of the correct type, but was not an allowed value."""
def __init__(self, value):
"""
>>> raise VdtValueError('jedi')
Traceback (most recent call last):
VdtValueError: the value "jedi" is unacceptable.
"""
ValidateError.__init__(self, 'the value "%s" is unacceptable.' % (value,))
class VdtValueTooSmallError(VdtValueError):
"""The value supplied was of the correct type, but was too small."""
def __init__(self, value):
"""
>>> raise VdtValueTooSmallError('0')
Traceback (most recent call last):
VdtValueTooSmallError: the value "0" is too small.
"""
ValidateError.__init__(self, 'the value "%s" is too small.' % (value,))
class VdtValueTooBigError(VdtValueError):
"""The value supplied was of the correct type, but was too big."""
def __init__(self, value):
"""
>>> raise VdtValueTooBigError('1')
Traceback (most recent call last):
VdtValueTooBigError: the value "1" is too big.
"""
ValidateError.__init__(self, 'the value "%s" is too big.' % (value,))
class VdtValueTooShortError(VdtValueError):
"""The value supplied was of the correct type, but was too short."""
def __init__(self, value):
"""
>>> raise VdtValueTooShortError('jed')
Traceback (most recent call last):
VdtValueTooShortError: the value "jed" is too short.
"""
ValidateError.__init__(
self,
'the value "%s" is too short.' % (value,))
class VdtValueTooLongError(VdtValueError):
"""The value supplied was of the correct type, but was too long."""
def __init__(self, value):
"""
>>> raise VdtValueTooLongError('jedie')
Traceback (most recent call last):
VdtValueTooLongError: the value "jedie" is too long.
"""
ValidateError.__init__(self, 'the value "%s" is too long.' % (value,))
class Validator(object):
"""
Validator is an object that allows you to register a set of 'checks'.
These checks take input and test that it conforms to the check.
This can also involve converting the value from a string into
the correct datatype.
The ``check`` method takes an input string which configures which
check is to be used and applies that check to a supplied value.
An example input string would be:
'int_range(param1, param2)'
You would then provide something like:
>>> def int_range_check(value, min, max):
... # turn min and max from strings to integers
... min = int(min)
... max = int(max)
... # check that value is of the correct type.
... # possible valid inputs are integers or strings
... # that represent integers
... if not isinstance(value, (int, long, basestring)):
... raise VdtTypeError(value)
... elif isinstance(value, basestring):
... # if we are given a string
... # attempt to convert to an integer
... try:
... value = int(value)
... except ValueError:
... raise VdtValueError(value)
... # check the value is between our constraints
... if not min <= value:
... raise VdtValueTooSmallError(value)
... if not value <= max:
... raise VdtValueTooBigError(value)
... return value
...
>>> fdict = {'int_range': int_range_check}
>>> vtr1 = Validator(fdict)
>>> vtr1.check('int_range(20, 40)', '30')
30
>>> vtr1.check('int_range(20, 40)', '60')
Traceback (most recent call last):
VdtValueTooBigError: the value "60" is too big.
New functions can be added with : ::
>>> vtr2 = Validator()
>>> vtr2.functions['int_range'] = int_range_check
Or by passing in a dictionary of functions when Validator
is instantiated.
Your functions *can* use keyword arguments,
but the first argument should always be 'value'.
If the function doesn't take additional arguments,
the parentheses are optional in the check.
It can be written with either of : ::
keyword = function_name
keyword = function_name()
The first program to utilise Validator() was Michael Foord's
ConfigObj, an alternative to ConfigParser which supports lists and
can validate a config file using a config schema.
For more details on using Validator with ConfigObj see:
http://www.voidspace.org.uk/python/configobj.html
"""
# this regex does the initial parsing of the checks
_func_re = re.compile(r'(.+?)\((.*)\)', re.DOTALL)
# this regex takes apart keyword arguments
_key_arg = re.compile(r'^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.*)$', re.DOTALL)
# this regex finds keyword=list(....) type values
_list_arg = _list_arg
# this regex takes individual values out of lists - in one pass
_list_members = _list_members
# These regexes check a set of arguments for validity
# and then pull the members out
_paramfinder = re.compile(_paramstring, re.VERBOSE | re.DOTALL)
_matchfinder = re.compile(_matchstring, re.VERBOSE | re.DOTALL)
def __init__(self, functions=None):
"""
>>> vtri = Validator()
"""
self.functions = {
'': self._pass,
'integer': is_integer,
'float': is_float,
'boolean': is_boolean,
'ip_addr': is_ip_addr,
'string': is_string,
'list': is_list,
'tuple': is_tuple,
'int_list': is_int_list,
'float_list': is_float_list,
'bool_list': is_bool_list,
'ip_addr_list': is_ip_addr_list,
'string_list': is_string_list,
'mixed_list': is_mixed_list,
'pass': self._pass,
'option': is_option,
'force_list': force_list,
}
if functions is not None:
self.functions.update(functions)
# tekNico: for use by ConfigObj
self.baseErrorClass = ValidateError
self._cache = {}
def check(self, check, value, missing=False):
"""
Usage: check(check, value)
Arguments:
check: string representing check to apply (including arguments)
value: object to be checked
Returns value, converted to correct type if necessary
If the check fails, raises a ``ValidateError`` subclass.
>>> vtor.check('yoda', '')
Traceback (most recent call last):
VdtUnknownCheckError: the check "yoda" is unknown.
>>> vtor.check('yoda()', '')
Traceback (most recent call last):
VdtUnknownCheckError: the check "yoda" is unknown.
>>> vtor.check('string(default="")', '', missing=True)
''
"""
fun_name, fun_args, fun_kwargs, default = self._parse_with_caching(check)
if missing:
if default is None:
# no information needed here - to be handled by caller
raise VdtMissingValue()
value = self._handle_none(default)
if value is None:
return None
return self._check_value(value, fun_name, fun_args, fun_kwargs)
def _handle_none(self, value):
if value == 'None':
value = None
elif value in ("'None'", '"None"'):
# Special case a quoted None
value = self._unquote(value)
return value
def _parse_with_caching(self, check):
if check in self._cache:
fun_name, fun_args, fun_kwargs, default = self._cache[check]
# We call list and dict below to work with *copies* of the data
# rather than the original (which are mutable of course)
fun_args = list(fun_args)
fun_kwargs = dict(fun_kwargs)
else:
fun_name, fun_args, fun_kwargs, default = self._parse_check(check)
fun_kwargs = dict([(str(key), value) for (key, value) in fun_kwargs.items()])
self._cache[check] = fun_name, list(fun_args), dict(fun_kwargs), default
return fun_name, fun_args, fun_kwargs, default
def _check_value(self, value, fun_name, fun_args, fun_kwargs):
try:
fun = self.functions[fun_name]
except KeyError:
raise VdtUnknownCheckError(fun_name)
else:
return fun(value, *fun_args, **fun_kwargs)
def _parse_check(self, check):
fun_match = self._func_re.match(check)
if fun_match:
fun_name = fun_match.group(1)
arg_string = fun_match.group(2)
arg_match = self._matchfinder.match(arg_string)
if arg_match is None:
# Bad syntax
raise VdtParamError('Bad syntax in check "%s".' % check)
fun_args = []
fun_kwargs = {}
# pull out args of group 2
for arg in self._paramfinder.findall(arg_string):
# args may need whitespace removing (before removing quotes)
arg = arg.strip()
listmatch = self._list_arg.match(arg)
if listmatch:
key, val = self._list_handle(listmatch)
fun_kwargs[key] = val
continue
keymatch = self._key_arg.match(arg)
if keymatch:
val = keymatch.group(2)
if not val in ("'None'", '"None"'):
# Special case a quoted None
val = self._unquote(val)
fun_kwargs[keymatch.group(1)] = val
continue
fun_args.append(self._unquote(arg))
else:
# allows for function names without (args)
return check, (), {}, None
# Default must be deleted if the value is specified too,
# otherwise the check function will get a spurious "default" keyword arg
try:
default = fun_kwargs.pop('default', None)
except AttributeError:
# Python 2.2 compatibility
default = None
try:
default = fun_kwargs['default']
del fun_kwargs['default']
except KeyError:
pass
return fun_name, fun_args, fun_kwargs, default
def _unquote(self, val):
"""Unquote a value if necessary."""
if (len(val) >= 2) and (val[0] in ("'", '"')) and (val[0] == val[-1]):
val = val[1:-1]
return val
def _list_handle(self, listmatch):
"""Take apart a ``keyword=list('val, 'val')`` type string."""
out = []
name = listmatch.group(1)
args = listmatch.group(2)
for arg in self._list_members.findall(args):
out.append(self._unquote(arg))
return name, out
def _pass(self, value):
"""
Dummy check that always passes
>>> vtor.check('', 0)
0
>>> vtor.check('', '0')
'0'
"""
return value
def get_default_value(self, check):
"""
Given a check, return the default value for the check
(converted to the right type).
If the check doesn't specify a default value then a
``KeyError`` will be raised.
"""
fun_name, fun_args, fun_kwargs, default = self._parse_with_caching(check)
if default is None:
raise KeyError('Check "%s" has no default value.' % check)
value = self._handle_none(default)
if value is None:
return value
return self._check_value(value, fun_name, fun_args, fun_kwargs)
def _is_num_param(names, values, to_float=False):
"""
Return numbers from inputs or raise VdtParamError.
Lets ``None`` pass through.
Pass in keyword argument ``to_float=True`` to
use float for the conversion rather than int.
>>> _is_num_param(('', ''), (0, 1.0))
[0, 1]
>>> _is_num_param(('', ''), (0, 1.0), to_float=True)
[0.0, 1.0]
>>> _is_num_param(('a'), ('a'))
Traceback (most recent call last):
VdtParamError: passed an incorrect value "a" for parameter "a".
"""
fun = to_float and float or int
out_params = []
for (name, val) in zip(names, values):
if val is None:
out_params.append(val)
elif isinstance(val, (int, long, float, basestring)):
try:
out_params.append(fun(val))
except ValueError, e:
raise VdtParamError(name, val)
else:
raise VdtParamError(name, val)
return out_params
# built in checks
# you can override these by setting the appropriate name
# in Validator.functions
# note: if the params are specified wrongly in your input string,
# you will also raise errors.
def is_integer(value, min=None, max=None):
"""
A check that tests that a given value is an integer (int, or long)
and optionally, between bounds. A negative value is accepted, while
a float will fail.
If the value is a string, then the conversion is done - if possible.
Otherwise a VdtError is raised.
>>> vtor.check('integer', '-1')
-1
>>> vtor.check('integer', '0')
0
>>> vtor.check('integer', 9)
9
>>> vtor.check('integer', 'a')
Traceback (most recent call last):
VdtTypeError: the value "a" is of the wrong type.
>>> vtor.check('integer', '2.2')
Traceback (most recent call last):
VdtTypeError: the value "2.2" is of the wrong type.
>>> vtor.check('integer(10)', '20')
20
>>> vtor.check('integer(max=20)', '15')
15
>>> vtor.check('integer(10)', '9')
Traceback (most recent call last):
VdtValueTooSmallError: the value "9" is too small.
>>> vtor.check('integer(10)', 9)
Traceback (most recent call last):
VdtValueTooSmallError: the value "9" is too small.
>>> vtor.check('integer(max=20)', '35')
Traceback (most recent call last):
VdtValueTooBigError: the value "35" is too big.
>>> vtor.check('integer(max=20)', 35)
Traceback (most recent call last):
VdtValueTooBigError: the value "35" is too big.
>>> vtor.check('integer(0, 9)', False)
0
"""
(min_val, max_val) = _is_num_param(('min', 'max'), (min, max))
if not isinstance(value, (int, long, basestring)):
raise VdtTypeError(value)
if isinstance(value, basestring):
# if it's a string - does it represent an integer ?
try:
value = int(value)
except ValueError:
raise VdtTypeError(value)
if (min_val is not None) and (value < min_val):
raise VdtValueTooSmallError(value)
if (max_val is not None) and (value > max_val):
raise VdtValueTooBigError(value)
return value
def is_float(value, min=None, max=None):
"""
A check that tests that a given value is a float
(an integer will be accepted), and optionally - that it is between bounds.
If the value is a string, then the conversion is done - if possible.
Otherwise a VdtError is raised.
This can accept negative values.
>>> vtor.check('float', '2')
2.0
From now on we multiply the value to avoid comparing decimals
>>> vtor.check('float', '-6.8') * 10
-68.0
>>> vtor.check('float', '12.2') * 10
122.0
>>> vtor.check('float', 8.4) * 10
84.0
>>> vtor.check('float', 'a')
Traceback (most recent call last):
VdtTypeError: the value "a" is of the wrong type.
>>> vtor.check('float(10.1)', '10.2') * 10
102.0
>>> vtor.check('float(max=20.2)', '15.1') * 10
151.0
>>> vtor.check('float(10.0)', '9.0')
Traceback (most recent call last):
VdtValueTooSmallError: the value "9.0" is too small.
>>> vtor.check('float(max=20.0)', '35.0')
Traceback (most recent call last):
VdtValueTooBigError: the value "35.0" is too big.
"""
(min_val, max_val) = _is_num_param(
('min', 'max'), (min, max), to_float=True)
if not isinstance(value, (int, long, float, basestring)):
raise VdtTypeError(value)
if not isinstance(value, float):
# if it's a string - does it represent a float ?
try:
value = float(value)
except ValueError:
raise VdtTypeError(value)
if (min_val is not None) and (value < min_val):
raise VdtValueTooSmallError(value)
if (max_val is not None) and (value > max_val):
raise VdtValueTooBigError(value)
return value
bool_dict = {
True: True, 'on': True, '1': True, 'true': True, 'yes': True,
False: False, 'off': False, '0': False, 'false': False, 'no': False,
}
def is_boolean(value):
"""
Check if the value represents a boolean.
>>> vtor.check('boolean', 0)
0
>>> vtor.check('boolean', False)
0
>>> vtor.check('boolean', '0')
0
>>> vtor.check('boolean', 'off')
0
>>> vtor.check('boolean', 'false')
0
>>> vtor.check('boolean', 'no')
0
>>> vtor.check('boolean', 'nO')
0
>>> vtor.check('boolean', 'NO')
0
>>> vtor.check('boolean', 1)
1
>>> vtor.check('boolean', True)
1
>>> vtor.check('boolean', '1')
1
>>> vtor.check('boolean', 'on')
1
>>> vtor.check('boolean', 'true')
1
>>> vtor.check('boolean', 'yes')
1
>>> vtor.check('boolean', 'Yes')
1
>>> vtor.check('boolean', 'YES')
1
>>> vtor.check('boolean', '')
Traceback (most recent call last):
VdtTypeError: the value "" is of the wrong type.
>>> vtor.check('boolean', 'up')
Traceback (most recent call last):
VdtTypeError: the value "up" is of the wrong type.
"""
if isinstance(value, basestring):
try:
return bool_dict[value.lower()]
except KeyError:
raise VdtTypeError(value)
# we do an equality test rather than an identity test
# this ensures Python 2.2 compatibilty
# and allows 0 and 1 to represent True and False
if value == False:
return False
elif value == True:
return True
else:
raise VdtTypeError(value)
def is_ip_addr(value):
"""
Check that the supplied value is an Internet Protocol address, v.4,
represented by a dotted-quad string, i.e. '1.2.3.4'.
>>> vtor.check('ip_addr', '1 ')
'1'
>>> vtor.check('ip_addr', ' 1.2')
'1.2'
>>> vtor.check('ip_addr', ' 1.2.3 ')
'1.2.3'
>>> vtor.check('ip_addr', '1.2.3.4')
'1.2.3.4'
>>> vtor.check('ip_addr', '0.0.0.0')
'0.0.0.0'
>>> vtor.check('ip_addr', '255.255.255.255')
'255.255.255.255'
>>> vtor.check('ip_addr', '255.255.255.256')
Traceback (most recent call last):
VdtValueError: the value "255.255.255.256" is unacceptable.
>>> vtor.check('ip_addr', '1.2.3.4.5')
Traceback (most recent call last):
VdtValueError: the value "1.2.3.4.5" is unacceptable.
>>> vtor.check('ip_addr', 0)
Traceback (most recent call last):
VdtTypeError: the value "0" is of the wrong type.
"""
if not isinstance(value, basestring):
raise VdtTypeError(value)
value = value.strip()
try:
dottedQuadToNum(value)
except ValueError:
raise VdtValueError(value)
return value
def is_list(value, min=None, max=None):
"""
Check that the value is a list of values.
You can optionally specify the minimum and maximum number of members.
It does no check on list members.
>>> vtor.check('list', ())
[]
>>> vtor.check('list', [])
[]
>>> vtor.check('list', (1, 2))
[1, 2]
>>> vtor.check('list', [1, 2])
[1, 2]
>>> vtor.check('list(3)', (1, 2))
Traceback (most recent call last):
VdtValueTooShortError: the value "(1, 2)" is too short.
>>> vtor.check('list(max=5)', (1, 2, 3, 4, 5, 6))
Traceback (most recent call last):
VdtValueTooLongError: the value "(1, 2, 3, 4, 5, 6)" is too long.
>>> vtor.check('list(min=3, max=5)', (1, 2, 3, 4))
[1, 2, 3, 4]
>>> vtor.check('list', 0)
Traceback (most recent call last):
VdtTypeError: the value "0" is of the wrong type.
>>> vtor.check('list', '12')
Traceback (most recent call last):
VdtTypeError: the value "12" is of the wrong type.
"""
(min_len, max_len) = _is_num_param(('min', 'max'), (min, max))
if isinstance(value, basestring):
raise VdtTypeError(value)
try:
num_members = len(value)
except TypeError:
raise VdtTypeError(value)
if min_len is not None and num_members < min_len:
raise VdtValueTooShortError(value)
if max_len is not None and num_members > max_len:
raise VdtValueTooLongError(value)
return list(value)
def is_tuple(value, min=None, max=None):
"""
Check that the value is a tuple of values.
You can optionally specify the minimum and maximum number of members.
It does no check on members.
>>> vtor.check('tuple', ())
()
>>> vtor.check('tuple', [])
()
>>> vtor.check('tuple', (1, 2))
(1, 2)
>>> vtor.check('tuple', [1, 2])
(1, 2)
>>> vtor.check('tuple(3)', (1, 2))
Traceback (most recent call last):
VdtValueTooShortError: the value "(1, 2)" is too short.
>>> vtor.check('tuple(max=5)', (1, 2, 3, 4, 5, 6))
Traceback (most recent call last):
VdtValueTooLongError: the value "(1, 2, 3, 4, 5, 6)" is too long.
>>> vtor.check('tuple(min=3, max=5)', (1, 2, 3, 4))
(1, 2, 3, 4)
>>> vtor.check('tuple', 0)
Traceback (most recent call last):
VdtTypeError: the value "0" is of the wrong type.
>>> vtor.check('tuple', '12')
Traceback (most recent call last):
VdtTypeError: the value "12" is of the wrong type.
"""
return tuple(is_list(value, min, max))
def is_string(value, min=None, max=None):
"""
Check that the supplied value is a string.
You can optionally specify the minimum and maximum number of members.
>>> vtor.check('string', '0')
'0'
>>> vtor.check('string', 0)
Traceback (most recent call last):
VdtTypeError: the value "0" is of the wrong type.
>>> vtor.check('string(2)', '12')
'12'
>>> vtor.check('string(2)', '1')
Traceback (most recent call last):
VdtValueTooShortError: the value "1" is too short.
>>> vtor.check('string(min=2, max=3)', '123')
'123'
>>> vtor.check('string(min=2, max=3)', '1234')
Traceback (most recent call last):
VdtValueTooLongError: the value "1234" is too long.
"""
if not isinstance(value, basestring):
raise VdtTypeError(value)
(min_len, max_len) = _is_num_param(('min', 'max'), (min, max))
try:
num_members = len(value)
except TypeError:
raise VdtTypeError(value)
if min_len is not None and num_members < min_len:
raise VdtValueTooShortError(value)
if max_len is not None and num_members > max_len:
raise VdtValueTooLongError(value)
return value
def is_int_list(value, min=None, max=None):
"""
Check that the value is a list of integers.
You can optionally specify the minimum and maximum number of members.
Each list member is checked that it is an integer.
>>> vtor.check('int_list', ())
[]
>>> vtor.check('int_list', [])
[]
>>> vtor.check('int_list', (1, 2))
[1, 2]
>>> vtor.check('int_list', [1, 2])
[1, 2]
>>> vtor.check('int_list', [1, 'a'])
Traceback (most recent call last):
VdtTypeError: the value "a" is of the wrong type.
"""
return [is_integer(mem) for mem in is_list(value, min, max)]
def is_bool_list(value, min=None, max=None):
"""
Check that the value is a list of booleans.
You can optionally specify the minimum and maximum number of members.
Each list member is checked that it is a boolean.
>>> vtor.check('bool_list', ())
[]
>>> vtor.check('bool_list', [])
[]
>>> check_res = vtor.check('bool_list', (True, False))
>>> check_res == [True, False]
1
>>> check_res = vtor.check('bool_list', [True, False])
>>> check_res == [True, False]
1
>>> vtor.check('bool_list', [True, 'a'])
Traceback (most recent call last):
VdtTypeError: the value "a" is of the wrong type.
"""
return [is_boolean(mem) for mem in is_list(value, min, max)]
def is_float_list(value, min=None, max=None):
"""
Check that the value is a list of floats.
You can optionally specify the minimum and maximum number of members.
Each list member is checked that it is a float.
>>> vtor.check('float_list', ())
[]
>>> vtor.check('float_list', [])
[]
>>> vtor.check('float_list', (1, 2.0))
[1.0, 2.0]
>>> vtor.check('float_list', [1, 2.0])
[1.0, 2.0]
>>> vtor.check('float_list', [1, 'a'])
Traceback (most recent call last):
VdtTypeError: the value "a" is of the wrong type.
"""
return [is_float(mem) for mem in is_list(value, min, max)]
def is_string_list(value, min=None, max=None):
"""
Check that the value is a list of strings.
You can optionally specify the minimum and maximum number of members.
Each list member is checked that it is a string.
>>> vtor.check('string_list', ())
[]
>>> vtor.check('string_list', [])
[]
>>> vtor.check('string_list', ('a', 'b'))
['a', 'b']
>>> vtor.check('string_list', ['a', 1])
Traceback (most recent call last):
VdtTypeError: the value "1" is of the wrong type.
>>> vtor.check('string_list', 'hello')
Traceback (most recent call last):
VdtTypeError: the value "hello" is of the wrong type.
"""
if isinstance(value, basestring):
raise VdtTypeError(value)
return [is_string(mem) for mem in is_list(value, min, max)]
def is_ip_addr_list(value, min=None, max=None):
"""
Check that the value is a list of IP addresses.
You can optionally specify the minimum and maximum number of members.
Each list member is checked that it is an IP address.
>>> vtor.check('ip_addr_list', ())
[]
>>> vtor.check('ip_addr_list', [])
[]
>>> vtor.check('ip_addr_list', ('1.2.3.4', '5.6.7.8'))
['1.2.3.4', '5.6.7.8']
>>> vtor.check('ip_addr_list', ['a'])
Traceback (most recent call last):
VdtValueError: the value "a" is unacceptable.
"""
return [is_ip_addr(mem) for mem in is_list(value, min, max)]
def force_list(value, min=None, max=None):
"""
Check that a value is a list, coercing strings into
a list with one member. Useful where users forget the
trailing comma that turns a single value into a list.
You can optionally specify the minimum and maximum number of members.
A minumum of greater than one will fail if the user only supplies a
string.
>>> vtor.check('force_list', ())
[]
>>> vtor.check('force_list', [])
[]
>>> vtor.check('force_list', 'hello')
['hello']
"""
if not isinstance(value, (list, tuple)):
value = [value]
return is_list(value, min, max)
fun_dict = {
'integer': is_integer,
'float': is_float,
'ip_addr': is_ip_addr,
'string': is_string,
'boolean': is_boolean,
}
def is_mixed_list(value, *args):
"""
Check that the value is a list.
Allow specifying the type of each member.
Work on lists of specific lengths.
You specify each member as a positional argument specifying type
Each type should be one of the following strings :
'integer', 'float', 'ip_addr', 'string', 'boolean'
So you can specify a list of two strings, followed by
two integers as :
mixed_list('string', 'string', 'integer', 'integer')
The length of the list must match the number of positional
arguments you supply.
>>> mix_str = "mixed_list('integer', 'float', 'ip_addr', 'string', 'boolean')"
>>> check_res = vtor.check(mix_str, (1, 2.0, '1.2.3.4', 'a', True))
>>> check_res == [1, 2.0, '1.2.3.4', 'a', True]
1
>>> check_res = vtor.check(mix_str, ('1', '2.0', '1.2.3.4', 'a', 'True'))
>>> check_res == [1, 2.0, '1.2.3.4', 'a', True]
1
>>> vtor.check(mix_str, ('b', 2.0, '1.2.3.4', 'a', True))
Traceback (most recent call last):
VdtTypeError: the value "b" is of the wrong type.
>>> vtor.check(mix_str, (1, 2.0, '1.2.3.4', 'a'))
Traceback (most recent call last):
VdtValueTooShortError: the value "(1, 2.0, '1.2.3.4', 'a')" is too short.
>>> vtor.check(mix_str, (1, 2.0, '1.2.3.4', 'a', 1, 'b'))
Traceback (most recent call last):
VdtValueTooLongError: the value "(1, 2.0, '1.2.3.4', 'a', 1, 'b')" is too long.
>>> vtor.check(mix_str, 0)
Traceback (most recent call last):
VdtTypeError: the value "0" is of the wrong type.
This test requires an elaborate setup, because of a change in error string
output from the interpreter between Python 2.2 and 2.3 .
>>> res_seq = (
... 'passed an incorrect value "',
... 'yoda',
... '" for parameter "mixed_list".',
... )
>>> res_str = "'".join(res_seq)
>>> try:
... vtor.check('mixed_list("yoda")', ('a'))
... except VdtParamError, err:
... str(err) == res_str
1
"""
try:
length = len(value)
except TypeError:
raise VdtTypeError(value)
if length < len(args):
raise VdtValueTooShortError(value)
elif length > len(args):
raise VdtValueTooLongError(value)
try:
return [fun_dict[arg](val) for arg, val in zip(args, value)]
except KeyError, e:
raise VdtParamError('mixed_list', e)
def is_option(value, *options):
"""
This check matches the value to any of a set of options.
>>> vtor.check('option("yoda", "jedi")', 'yoda')
'yoda'
>>> vtor.check('option("yoda", "jedi")', 'jed')
Traceback (most recent call last):
VdtValueError: the value "jed" is unacceptable.
>>> vtor.check('option("yoda", "jedi")', 0)
Traceback (most recent call last):
VdtTypeError: the value "0" is of the wrong type.
"""
if not isinstance(value, basestring):
raise VdtTypeError(value)
if not value in options:
raise VdtValueError(value)
return value
def _test(value, *args, **keywargs):
"""
A function that exists for test purposes.
>>> checks = [
... '3, 6, min=1, max=3, test=list(a, b, c)',
... '3',
... '3, 6',
... '3,',
... 'min=1, test="a b c"',
... 'min=5, test="a, b, c"',
... 'min=1, max=3, test="a, b, c"',
... 'min=-100, test=-99',
... 'min=1, max=3',
... '3, 6, test="36"',
... '3, 6, test="a, b, c"',
... '3, max=3, test=list("a", "b", "c")',
... '''3, max=3, test=list("'a'", 'b', "x=(c)")''',
... "test='x=fish(3)'",
... ]
>>> v = Validator({'test': _test})
>>> for entry in checks:
... print v.check(('test(%s)' % entry), 3)
(3, ('3', '6'), {'test': ['a', 'b', 'c'], 'max': '3', 'min': '1'})
(3, ('3',), {})
(3, ('3', '6'), {})
(3, ('3',), {})
(3, (), {'test': 'a b c', 'min': '1'})
(3, (), {'test': 'a, b, c', 'min': '5'})
(3, (), {'test': 'a, b, c', 'max': '3', 'min': '1'})
(3, (), {'test': '-99', 'min': '-100'})
(3, (), {'max': '3', 'min': '1'})
(3, ('3', '6'), {'test': '36'})
(3, ('3', '6'), {'test': 'a, b, c'})
(3, ('3',), {'test': ['a', 'b', 'c'], 'max': '3'})
(3, ('3',), {'test': ["'a'", 'b', 'x=(c)'], 'max': '3'})
(3, (), {'test': 'x=fish(3)'})
>>> v = Validator()
>>> v.check('integer(default=6)', '3')
3
>>> v.check('integer(default=6)', None, True)
6
>>> v.get_default_value('integer(default=6)')
6
>>> v.get_default_value('float(default=6)')
6.0
>>> v.get_default_value('pass(default=None)')
>>> v.get_default_value("string(default='None')")
'None'
>>> v.get_default_value('pass')
Traceback (most recent call last):
KeyError: 'Check "pass" has no default value.'
>>> v.get_default_value('pass(default=list(1, 2, 3, 4))')
['1', '2', '3', '4']
>>> v = Validator()
>>> v.check("pass(default=None)", None, True)
>>> v.check("pass(default='None')", None, True)
'None'
>>> v.check('pass(default="None")', None, True)
'None'
>>> v.check('pass(default=list(1, 2, 3, 4))', None, True)
['1', '2', '3', '4']
Bug test for unicode arguments
>>> v = Validator()
>>> v.check(u'string(min=4)', u'test')
u'test'
>>> v = Validator()
>>> v.get_default_value(u'string(min=4, default="1234")')
u'1234'
>>> v.check(u'string(min=4, default="1234")', u'test')
u'test'
>>> v = Validator()
>>> default = v.get_default_value('string(default=None)')
>>> default == None
1
"""
return (value, args, keywargs)
def _test2():
"""
>>>
>>> v = Validator()
>>> v.get_default_value('string(default="#ff00dd")')
'#ff00dd'
>>> v.get_default_value('integer(default=3) # comment')
3
"""
def _test3():
r"""
>>> vtor.check('string(default="")', '', missing=True)
''
>>> vtor.check('string(default="\n")', '', missing=True)
'\n'
>>> print vtor.check('string(default="\n")', '', missing=True),
>>> vtor.check('string()', '\n')
'\n'
>>> vtor.check('string(default="\n\n\n")', '', missing=True)
'\n\n\n'
>>> vtor.check('string()', 'random \n text goes here\n\n')
'random \n text goes here\n\n'
>>> vtor.check('string(default=" \nrandom text\ngoes \n here\n\n ")',
... '', missing=True)
' \nrandom text\ngoes \n here\n\n '
>>> vtor.check("string(default='\n\n\n')", '', missing=True)
'\n\n\n'
>>> vtor.check("option('\n','a','b',default='\n')", '', missing=True)
'\n'
>>> vtor.check("string_list()", ['foo', '\n', 'bar'])
['foo', '\n', 'bar']
>>> vtor.check("string_list(default=list('\n'))", '', missing=True)
['\n']
"""
if __name__ == '__main__':
# run the code tests in doctest format
import sys
import doctest
m = sys.modules.get('__main__')
globs = m.__dict__.copy()
globs.update({
'vtor': Validator(),
})
doctest.testmod(m, globs=globs)
terminator-1.91/terminatorlib/encoding.py 0000664 0001750 0001750 00000011511 13054612071 021141 0 ustar steve steve 0000000 0000000 #!/usr/bin/env python2
# TerminatorEncoding - charset encoding classes
# 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
# 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
"""TerminatorEncoding by Emmanuel Bretelle
TerminatorEncoding supplies a list of possible encoding
values.
This list is taken from gnome-terminal's src/terminal-encoding.c
and src/encoding.c
"""
from translation import _
#pylint: disable-msg=R0903
class TerminatorEncoding:
"""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") ]
]
def __init__(self):
pass
def get_list():
"""Return a list of supported encodings"""
return TerminatorEncoding.encodings
get_list = staticmethod(get_list)
terminator-1.91/terminatorlib/ipc.py 0000664 0001750 0001750 00000022154 13054612071 020133 0 ustar steve steve 0000000 0000000 #!/usr/bin/env python2
# Terminator by Chris Jones
# GPL v2 only
"""ipc.py - DBus server and API calls"""
from gi.repository import Gdk
import dbus.service
from dbus.exceptions import DBusException
import dbus.glib
from borg import Borg
from terminator import Terminator
from config import Config
from factory import Factory
from util import dbg, enumerate_descendants
CONFIG = Config()
if not CONFIG['dbus']:
# The config says we are not to load dbus, so pretend like we can't
dbg('dbus disabled')
raise ImportError
BUS_BASE = 'net.tenshu.Terminator2'
BUS_PATH = '/net/tenshu/Terminator2'
try:
# Try and include the X11 display name in the dbus bus name
DISPLAY = hex(hash(Gdk.get_display().partition('.')[0]))
BUS_NAME = '%s%s' % (BUS_BASE, DISPLAY)
except:
BUS_NAME = BUS_BASE
class DBusService(Borg, dbus.service.Object):
"""DBus Server class. This is implemented as a Borg"""
bus_name = None
bus_path = None
terminator = None
def __init__(self):
"""Class initialiser"""
Borg.__init__(self, self.__class__.__name__)
self.prepare_attributes()
dbus.service.Object.__init__(self, self.bus_name, BUS_PATH)
def prepare_attributes(self):
"""Ensure we are populated"""
if not self.bus_name:
dbg('Checking for bus name availability: %s' % BUS_NAME)
bus = dbus.SessionBus()
proxy = bus.get_object('org.freedesktop.DBus',
'/org/freedesktop/DBus')
flags = 1 | 4 # allow replacement | do not queue
if not proxy.RequestName(BUS_NAME, dbus.UInt32(flags)) in (1, 4):
dbg('bus name unavailable: %s' % BUS_NAME)
raise dbus.exceptions.DBusException(
"Couldn't get DBus name %s: Name exists" % BUS_NAME)
self.bus_name = dbus.service.BusName(BUS_NAME,
bus=dbus.SessionBus())
if not self.bus_path:
self.bus_path = BUS_PATH
if not self.terminator:
self.terminator = Terminator()
@dbus.service.method(BUS_NAME, in_signature='a{ss}')
def new_window_cmdline(self, options=dbus.Dictionary()):
"""Create a new Window"""
dbg('dbus method called: new_window with parameters %s'%(options))
oldopts = self.terminator.config.options_get()
oldopts.__dict__ = options
self.terminator.config.options_set(oldopts)
self.terminator.create_layout(oldopts.layout)
self.terminator.layout_done()
@dbus.service.method(BUS_NAME, in_signature='a{ss}')
def new_tab_cmdline(self, options=dbus.Dictionary()):
"""Create a new tab"""
dbg('dbus method called: new_tab with parameters %s'%(options))
oldopts = self.terminator.config.options_get()
oldopts.__dict__ = options
self.terminator.config.options_set(oldopts)
window = self.terminator.get_windows()[0]
window.tab_new()
@dbus.service.method(BUS_NAME)
def new_window(self):
"""Create a new Window"""
terminals_before = set(self.get_terminals())
self.terminator.new_window()
terminals_after = set(self.get_terminals())
new_terminal_set = list(terminals_after - terminals_before)
if len(new_terminal_set) != 1:
return "ERROR: Cannot determine the UUID of the added terminal"
else:
return new_terminal_set[0]
@dbus.service.method(BUS_NAME)
def new_tab(self, uuid=None):
"""Create a new tab"""
return self.new_terminal(uuid, 'tab')
@dbus.service.method(BUS_NAME)
def hsplit(self, uuid=None):
"""Split a terminal horizontally, by UUID"""
return self.new_terminal(uuid, 'hsplit')
@dbus.service.method(BUS_NAME)
def vsplit(self, uuid=None):
"""Split a terminal vertically, by UUID"""
return self.new_terminal(uuid, 'vsplit')
def new_terminal(self, uuid, type):
"""Split a terminal horizontally or vertically, by UUID"""
dbg('dbus method called: %s' % type)
if not uuid:
return "ERROR: No UUID specified"
terminal = self.terminator.find_terminal_by_uuid(uuid)
terminals_before = set(self.get_terminals())
if not terminal:
return "ERROR: Terminal with supplied UUID not found"
elif type == 'tab':
terminal.key_new_tab()
elif type == 'hsplit':
terminal.key_split_horiz()
elif type == 'vsplit':
terminal.key_split_vert()
else:
return "ERROR: Unknown type \"%s\" specified" % (type)
terminals_after = set(self.get_terminals())
# Detect the new terminal UUID
new_terminal_set = list(terminals_after - terminals_before)
if len(new_terminal_set) != 1:
return "ERROR: Cannot determine the UUID of the added terminal"
else:
return new_terminal_set[0]
@dbus.service.method(BUS_NAME)
def get_terminals(self):
"""Return a list of all the terminals"""
return [x.uuid.urn for x in self.terminator.terminals]
@dbus.service.method(BUS_NAME)
def get_window(self, uuid=None):
"""Return the UUID of the parent window of a given terminal"""
terminal = self.terminator.find_terminal_by_uuid(uuid)
window = terminal.get_toplevel()
return window.uuid.urn
@dbus.service.method(BUS_NAME)
def get_window_title(self, uuid=None):
"""Return the title of a parent window of a given terminal"""
terminal = self.terminator.find_terminal_by_uuid(uuid)
window = terminal.get_toplevel()
return window.get_title()
@dbus.service.method(BUS_NAME)
def get_tab(self, uuid=None):
"""Return the UUID of the parent tab of a given terminal"""
maker = Factory()
terminal = self.terminator.find_terminal_by_uuid(uuid)
window = terminal.get_toplevel()
root_widget = window.get_children()[0]
if maker.isinstance(root_widget, 'Notebook'):
#return root_widget.uuid.urn
for tab_child in root_widget.get_children():
terms = [tab_child]
if not maker.isinstance(terms[0], "Terminal"):
terms = enumerate_descendants(tab_child)[1]
if terminal in terms:
# FIXME: There are no uuid's assigned to the the notebook, or the actual tabs!
# This would fail: return root_widget.uuid.urn
return ""
@dbus.service.method(BUS_NAME)
def get_tab_title(self, uuid=None):
"""Return the title of a parent tab of a given terminal"""
maker = Factory()
terminal = self.terminator.find_terminal_by_uuid(uuid)
window = terminal.get_toplevel()
root_widget = window.get_children()[0]
if maker.isinstance(root_widget, "Notebook"):
for tab_child in root_widget.get_children():
terms = [tab_child]
if not maker.isinstance(terms[0], "Terminal"):
terms = enumerate_descendants(tab_child)[1]
if terminal in terms:
return root_widget.get_tab_label(tab_child).get_label()
def with_proxy(func):
"""Decorator function to connect to the session dbus bus"""
dbg('dbus client call: %s' % func.func_name)
def _exec(*args, **argd):
bus = dbus.SessionBus()
proxy = bus.get_object(BUS_NAME, BUS_PATH)
func(proxy, *args, **argd)
return _exec
@with_proxy
def new_window_cmdline(session, options):
"""Call the dbus method to open a new window"""
session.new_window_cmdline(options)
@with_proxy
def new_tab_cmdline(session, options):
"""Call the dbus method to open a new tab in the first window"""
session.new_tab_cmdline(options)
@with_proxy
def new_window(session, options):
"""Call the dbus method to open a new window"""
print session.new_window()
@with_proxy
def new_tab(session, uuid, options):
"""Call the dbus method to open a new tab in the first window"""
print session.new_tab(uuid)
@with_proxy
def hsplit(session, uuid, options):
"""Call the dbus method to horizontally split a terminal"""
print session.hsplit(uuid)
@with_proxy
def vsplit(session, uuid, options):
"""Call the dbus method to vertically split a terminal"""
print session.vsplit(uuid)
@with_proxy
def get_terminals(session, options):
"""Call the dbus method to return a list of all terminals"""
print '\n'.join(session.get_terminals())
@with_proxy
def get_window(session, uuid, options):
"""Call the dbus method to return the toplevel tab for a terminal"""
print session.get_window(uuid)
@with_proxy
def get_window_title(session, uuid, options):
"""Call the dbus method to return the title of a tab"""
print session.get_window_title(uuid)
@with_proxy
def get_tab(session, uuid, options):
"""Call the dbus method to return the toplevel tab for a terminal"""
print session.get_tab(uuid)
@with_proxy
def get_tab_title(session, uuid, options):
"""Call the dbus method to return the title of a tab"""
print session.get_tab_title(uuid)
terminator-1.91/terminatorlib/searchbar.py 0000775 0001750 0001750 00000016327 13054612071 021322 0 ustar steve steve 0000000 0000000 #!/usr/bin/env python2
# Terminator by Chris Jones
# GPL v2 only
"""searchbar.py - classes necessary to provide a terminal search bar"""
from gi.repository import Gtk, Gdk
from gi.repository import GObject
import re
from translation import _
from config import Config
# pylint: disable-msg=R0904
class Searchbar(Gtk.HBox):
"""Class implementing the Searchbar widget"""
__gsignals__ = {
'end-search': (GObject.SignalFlags.RUN_LAST, None, ()),
}
entry = None
reslabel = None
next = None
prev = None
wrap = None
vte = None
config = None
searchstring = None
searchre = None
searchrow = None
searchits = None
def __init__(self):
"""Class initialiser"""
GObject.GObject.__init__(self)
self.config = Config()
self.get_style_context().add_class("terminator-terminal-searchbar")
# 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(label=_('Search:'))
label.show()
# Result label
self.reslabel = Gtk.Label(label='')
self.reslabel.show()
# Close Button
close = Gtk.Button()
close.set_relief(Gtk.ReliefStyle.NONE)
close.set_focus_on_click(False)
icon = Gtk.Image()
icon.set_from_stock(Gtk.STOCK_CLOSE, Gtk.IconSize.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)
# Wrap checkbox
self.wrap = Gtk.CheckButton(_('Wrap'))
self.wrap.show()
self.wrap.set_sensitive(True)
self.wrap.connect('toggled', self.wrap_toggled)
self.pack_start(label, False, True, 0)
self.pack_start(self.entry, True, True, 0)
self.pack_start(self.reslabel, False, True, 0)
self.pack_start(self.prev, False, False, 0)
self.pack_start(self.next, False, False, 0)
self.pack_start(self.wrap, False, False, 0)
self.pack_end(close, False, False, 0)
self.hide()
self.set_no_show_all(True)
def wrap_toggled(self, toggled):
if self.searchrow is None:
self.prev.set_sensitive(False)
self.next.set_sensitive(False)
elif toggled:
self.prev.set_sensitive(True)
self.next.set_sensitive(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 = Gdk.keyval_name(event.keyval)
if key == 'Escape':
self.end_search()
else:
self.prev.set_sensitive(False)
self.next.set_sensitive(False)
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] - 1
self.searchstring = searchtext
self.searchre = re.compile(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()
found = startrow <= self.searchrow and self.searchrow < endrow
row = self.searchrow
while True:
row += 1
if row >= endrow:
if found and self.wrap.get_active():
row = startrow - 1
else:
self.prev.set_sensitive(found)
self.next.set_sensitive(False)
self.reslabel.set_text(_('No more results'))
return
buffer = self.vte.get_text_range(row, 0, row + 1, 0, self.search_character)
buffer = buffer[0]
buffer = buffer[:buffer.find('\n')]
matches = self.searchre.search(buffer)
if matches:
self.searchrow = row
self.prev.set_sensitive(True)
self.search_hit(self.searchrow)
return
def prev_search(self, widget):
"""Jump back to the previous search"""
startrow,endrow = self.get_vte_buffer_range()
found = startrow <= self.searchrow and self.searchrow < endrow
row = self.searchrow
while True:
row -= 1
if row <= startrow:
if found and self.wrap.get_active():
row = endrow
else:
self.next.set_sensitive(found)
self.prev.set_sensitive(False)
self.reslabel.set_text(_('No more results'))
return
buffer = self.vte.get_text_range(row, 0, row + 1, 0, self.search_character)
buffer = buffer[0]
buffer = buffer[:buffer.find('\n')]
matches = self.searchre.search(buffer)
if matches:
self.searchrow = row
self.next.set_sensitive(True)
self.search_hit(self.searchrow)
return
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):
"""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()
if self.config['scrollback_infinite']:
startrow = 0
else:
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.searchre = 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)
terminator-1.91/terminatorlib/prefseditor.py 0000775 0001750 0001750 00000201457 13054612071 021716 0 ustar steve steve 0000000 0000000 #!/usr/bin/env python2
"""Preferences Editor for Terminator.
Load a UIBuilder config file, display it,
populate it with our current config, then optionally read that back out and
write it to a config file
"""
import os
from gi.repository import GObject, Gtk, Gdk
from util import dbg, err
import config
from keybindings import Keybindings, KeymapError
from translation import _
from encoding import TerminatorEncoding
from terminator import Terminator
from plugin import PluginRegistry
from version import APP_NAME
def color2hex(widget):
"""Pull the colour values out of a Gtk ColorPicker widget and return them
as 8bit hex values, sinces its default behaviour is to give 16bit values"""
widcol = widget.get_color()
return('#%02x%02x%02x' % (widcol.red>>8, widcol.green>>8, widcol.blue>>8))
# FIXME: We need to check that we have represented all of Config() below
class PrefsEditor:
"""Class implementing the various parts of the preferences editor"""
config = None
registry = None
plugins = None
keybindings = None
window = None
builder = None
layouteditor = None
previous_layout_selection = None
previous_profile_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,
'ambience': 6,
'solarized_light': 7,
'solarized_dark': 8,
'gruvbox_light': 9,
'gruvbox_dark': 10,
'custom': 11}
colourschemes = {'grey_on_black': ['#aaaaaa', '#000000'],
'black_on_yellow': ['#000000', '#ffffdd'],
'black_on_white': ['#000000', '#ffffff'],
'white_on_black': ['#ffffff', '#000000'],
'green_on_black': ['#00ff00', '#000000'],
'orange_on_black': ['#e53c00', '#000000'],
'ambience': ['#ffffff', '#300a24'],
'solarized_light': ['#657b83', '#fdf6e3'],
'solarized_dark': ['#839496', '#002b36'],
'gruvbox_light': ['#3c3836', '#fbf1c7'],
'gruvbox_dark': ['#ebdbb2', '#282828']}
palettevalues = {'tango': 0,
'linux': 1,
'xterm': 2,
'rxvt': 3,
'ambience': 4,
'solarized': 5,
'gruvbox_light': 6,
'gruvbox_dark': 7,
'custom': 8}
palettes = {'tango': '#000000:#cc0000:#4e9a06:#c4a000:#3465a4:\
#75507b:#06989a:#d3d7cf:#555753:#ef2929:#8ae234:#fce94f:#729fcf:\
#ad7fa8:#34e2e2:#eeeeec',
'linux': '#000000:#aa0000:#00aa00:#aa5500:#0000aa:\
#aa00aa:#00aaaa:#aaaaaa:#555555:#ff5555:#55ff55:#ffff55:#5555ff:\
#ff55ff:#55ffff:#ffffff',
'xterm': '#000000:#cd0000:#00cd00:#cdcd00:#0000ee:\
#cd00cd:#00cdcd:#e5e5e5:#7f7f7f:#ff0000:#00ff00:#ffff00:#5c5cff:\
#ff00ff:#00ffff:#ffffff',
'rxvt': '#000000:#cd0000:#00cd00:#cdcd00:#0000cd:\
#cd00cd:#00cdcd:#faebd7:#404040:#ff0000:#00ff00:#ffff00:#0000ff:\
#ff00ff:#00ffff:#ffffff',
'ambience': '#2e3436:#cc0000:#4e9a06:#c4a000:\
#3465a4:#75507b:#06989a:#d3d7cf:#555753:#ef2929:#8ae234:#fce94f:\
#729fcf:#ad7fa8:#34e2e2:#eeeeec',
'solarized': '#073642:#dc322f:#859900:#b58900:\
#268bd2:#d33682:#2aa198:#eee8d5:#002b36:#cb4b16:#586e75:#657b83:\
#839496:#6c71c4:#93a1a1:#fdf6e3',
'gruvbox_light': '#fbf1c7:#cc241d:#98971a:#d79921:\
#458588:#b16286:#689d6a:#7c6f64:#928374:#9d0006:#79740e:#b57614:\
#076678:#8f3f71:#427b58:#3c3836',
'gruvbox_dark': '#282828:#cc241d:#98971a:#d79921:\
#458588:#b16286:#689d6a:#a89984:#928374:#fb4934:#b8bb26:#fabd2f:\
#83a598:#d3869b:#8ec07c:#ebdbb2'}
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'),
'rotate_cw' : _('Rotate terminals clockwise'),
'rotate_ccw' : _('Rotate terminals counter-clockwise'),
'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'),
'page_up' : _('Scroll upwards one page'),
'page_down' : _('Scroll downwards one page'),
'page_up_half' : _('Scroll upwards half a page'),
'page_down_half' : _('Scroll downwards half a page'),
'line_up' : _('Scroll upwards one line'),
'line_down' : _('Scroll downwards one line'),
'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' : _('Maximize 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'),
'group_all_toggle' : _('Group/Ungroup all terminals'),
'ungroup_all' : _('Ungroup all terminals'),
'group_tab' : _('Group terminals in tab'),
'group_tab_toggle' : _('Group/Ungroup terminals in tab'),
'ungroup_tab' : _('Ungroup terminals in tab'),
'new_window' : _('Create a new window'),
'new_terminator' : _('Spawn a new Terminator process'),
'broadcast_off' : _('Don\'t broadcast key presses'),
'broadcast_group' : _('Broadcast key presses to group'),
'broadcast_all' : _('Broadcast key events to all'),
'insert_number' : _('Insert terminal number'),
'insert_padded' : _('Insert padded terminal number'),
'edit_window_title': _('Edit window title'),
'edit_terminal_title': _('Edit terminal title'),
'edit_tab_title' : _('Edit tab title'),
'layout_launcher' : _('Open layout launcher window'),
'next_profile' : _('Switch to next profile'),
'previous_profile' : _('Switch to previous profile'),
'help' : _('Open the manual')
}
def __init__ (self, term):
self.config = config.Config()
self.config.base.reload()
self.term = term
self.builder = Gtk.Builder()
self.builder.set_translation_domain(APP_NAME)
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')
icon_theme = Gtk.IconTheme.get_default()
if icon_theme.lookup_icon('terminator-preferences', 48, 0):
self.window.set_icon_name('terminator-preferences')
else:
dbg('Unable to load Terminator preferences icon')
icon = self.window.render_icon(Gtk.STOCK_DIALOG_INFO, Gtk.IconSize.BUTTON)
self.window.set_icon(icon)
self.layouteditor = LayoutEditor(self.builder)
self.builder.connect_signals(self)
self.layouteditor.prepare()
self.window.show_all()
try:
self.config.inhibit_save()
self.set_values()
except Exception, e:
err('Unable to set values: %s' % e)
self.config.uninhibit_save()
def on_closebutton_clicked(self, _button):
"""Close the window"""
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))
widget = guiget('handlesize_value_label')
widget.set_text(str(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'])
# Extra styling
widget = guiget('extrastylingcheck')
widget.set_active(self.config['extra_styling'])
# 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
elif option == 'hidden':
active = 4
else:
active = 0
widget.set_active(active)
# Broadcast default
option = self.config['broadcast_default']
widget = guiget('broadcastdefault')
if option == 'all':
active = 0
elif option == 'off':
active = 2
else:
active = 1
widget.set_active(active)
# scroll_tabbar
widget = guiget('scrolltabbarcheck')
widget.set_active(self.config['scroll_tabbar'])
# homogeneous_tabbar
widget = guiget('homogeneouscheck')
widget.set_active(self.config['homogeneous_tabbar'])
# DBus Server
widget = guiget('dbuscheck')
widget.set_active(self.config['dbus'])
#Hide from taskbar
widget = guiget('hidefromtaskbcheck')
widget.set_active(self.config['hide_from_taskbar'])
#Always on top
widget = guiget('alwaysontopcheck')
widget.set_active(self.config['always_on_top'])
#Hide on lose focus
widget = guiget('hideonlosefocuscheck')
widget.set_active(self.config['hide_on_lose_focus'])
#Show on all workspaces
widget = guiget('stickycheck')
widget.set_active(self.config['sticky'])
#Hide size text from the title bar
widget = guiget('title_hide_sizetextcheck')
widget.set_active(self.config['title_hide_sizetext'])
#Always split with profile
widget = guiget('always_split_with_profile')
widget.set_active(self.config['always_split_with_profile'])
# Putty paste style
widget = guiget('putty_paste_style')
widget.set_active(self.config['putty_paste_style'])
# Smart copy
widget = guiget('smart_copy')
widget.set_active(self.config['smart_copy'])
#Titlebar font selector
# Use system font
widget = guiget('title_system_font_checkbutton')
widget.set_active(self.config['title_use_system_font'])
self.on_title_system_font_checkbutton_toggled(widget)
# Font selector
widget = guiget('title_font_selector')
if self.config['title_use_system_font'] == True:
fontname = self.config.get_system_prop_font()
if fontname is not None:
widget.set_font_name(fontname)
else:
widget.set_font_name(self.config['title_font'])
## Profile tab
# Populate the profile list
widget = guiget('profilelist')
liststore = widget.get_model()
profiles = self.config.list_profiles()
self.profileiters = {}
for profile in profiles:
if profile == 'default':
editable = False
else:
editable = True
self.profileiters[profile] = liststore.append([profile, editable])
selection = widget.get_selection()
selection.connect('changed', self.on_profile_selection_changed)
selection.select_iter(self.profileiters['default'])
## Layouts tab
widget = guiget('layoutlist')
liststore = widget.get_model()
layouts = self.config.list_layouts()
self.layoutiters = {}
for layout in layouts:
if layout == 'default':
editable = False
else:
editable = True
self.layoutiters[layout] = liststore.append([layout, editable])
selection = widget.get_selection()
selection.connect('changed', self.on_layout_selection_changed)
terminator = Terminator()
if terminator.layoutname:
layout_to_highlight = terminator.layoutname
else:
layout_to_highlight = 'default'
selection.select_iter(self.layoutiters[layout_to_highlight])
# Now set up the selection changed handler for the layout itself
widget = guiget('LayoutTreeView')
selection = widget.get_selection()
selection.connect('changed', self.on_layout_item_selection_changed)
## Keybindings tab
widget = guiget('keybindingtreeview')
liststore = widget.get_model()
liststore.set_sort_column_id(0, Gtk.SortType.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
# Populate the plugin list
widget = guiget('pluginlist')
liststore = widget.get_model()
self.registry = PluginRegistry()
self.pluginiters = {}
pluginlist = self.registry.get_available_plugins()
self.plugins = {}
for plugin in pluginlist:
self.plugins[plugin] = self.registry.is_enabled(plugin)
for plugin in self.plugins:
self.pluginiters[plugin] = liststore.append([plugin,
self.plugins[plugin]])
selection = widget.get_selection()
selection.connect('changed', self.on_plugin_selection_changed)
if len(self.pluginiters) > 0:
selection.select_iter(liststore.get_iter_first())
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')
if self.config['use_system_font'] == True:
fontname = self.config.get_system_mono_font()
if fontname is not None:
widget.set_font_name(fontname)
else:
widget.set_font_name(self.config['font'])
# Allow bold text
widget = guiget('allow_bold_checkbutton')
widget.set_active(self.config['allow_bold'])
# Icon terminal bell
widget = guiget('icon_bell_checkbutton')
widget.set_active(self.config['icon_bell'])
# 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'])
# Show titlebar
widget = guiget('show_titlebar')
widget.set_active(self.config['show_titlebar'])
# Copy on selection
widget = guiget('copy_on_selection')
widget.set_active(self.config['copy_on_selection'])
# Rewrap on resize
widget = guiget('rewrap_on_resize_checkbutton')
widget.set_active(self.config['rewrap_on_resize'])
# Word chars
widget = guiget('word_chars_entry')
widget.set_text(self.config['word_chars'])
# Word char support was missing from vte 0.38, hide from the UI
if not hasattr(self.term.vte, 'set_word_char_exceptions'):
guiget('word_chars_hbox').hide()
# 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)
# Cursor blink
widget = guiget('cursor_blink')
widget.set_active(self.config['cursor_blink'])
# Cursor colour - Radio values
if self.config['cursor_color_fg']:
widget = guiget('cursor_color_foreground_radiobutton')
else:
widget = guiget('cursor_color_custom_radiobutton')
widget.set_active(True)
# Cursor colour - swatch
widget = guiget('cursor_color')
widget.set_sensitive(not self.config['cursor_color_fg'])
try:
widget.set_color(Gdk.color_parse(self.config['cursor_color']))
except (ValueError, TypeError):
try:
self.config['cursor_color'] = self.config['foreground_color']
widget.set_color(Gdk.color_parse(self.config['cursor_color']))
except ValueError:
self.config['cursor_color'] = "#FFFFFF"
widget.set_color(Gdk.color_parse(self.config['cursor_color']))
## Command tab
# Login shell
widget = guiget('login_shell_checkbutton')
widget.set_active(self.config['login_shell'])
# 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 = None
for ascheme in self.colourschemes:
forecol = self.colourschemes[ascheme][0]
backcol = self.colourschemes[ascheme][1]
if self.config['foreground_color'].lower() == forecol and \
self.config['background_color'].lower() == backcol:
scheme = ascheme
break
if scheme not in self.colorschemevalues:
if self.config['foreground_color'] in [None, ''] or \
self.config['background_color'] in [None, '']:
scheme = 'grey_on_black'
else:
scheme = 'custom'
# NOTE: The scheme is set in the GUI widget after the fore/back colours
# Foreground color
widget = guiget('foreground_colorpicker')
widget.set_color(Gdk.color_parse(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(Gdk.color_parse(self.config['background_color']))
if scheme == 'custom':
widget.set_sensitive(True)
else:
widget.set_sensitive(False)
# Now actually set the scheme
widget = guiget('color_scheme_combobox')
widget.set_active(self.colorschemevalues[scheme])
# Palette scheme
widget = guiget('palette_combobox')
palette = None
for apalette in self.palettes:
if self.config['palette'].lower() == self.palettes[apalette]:
palette = apalette
if palette not in self.palettevalues:
if self.config['palette'] in [None, '']:
palette = 'rxvt'
else:
palette = 'custom'
# NOTE: The palette selector is set after the colour pickers
# Palette colour pickers
colourpalette = self.config['palette'].split(':')
for i in xrange(1, 17):
widget = guiget('palette_colorpicker_%d' % i)
widget.set_color(Gdk.color_parse(colourpalette[i - 1]))
# Now set the palette selector widget
widget = guiget('palette_combobox')
widget.set_active(self.palettevalues[palette])
# Titlebar colors
for bit in ['title_transmit_fg_color', 'title_transmit_bg_color',
'title_receive_fg_color', 'title_receive_bg_color',
'title_inactive_fg_color', 'title_inactive_bg_color']:
widget = guiget(bit)
widget.set_color(Gdk.color_parse(self.config[bit]))
# Inactive terminal shading
widget = guiget('inactive_color_offset')
widget.set_value(float(self.config['inactive_color_offset']))
widget = guiget('inactive_color_offset_value_label')
widget.set_text('%d%%' % (int(float(self.config['inactive_color_offset'])*100)))
# Use custom URL handler
widget = guiget('use_custom_url_handler_checkbox')
widget.set_active(self.config['use_custom_url_handler'])
self.on_use_custom_url_handler_checkbutton_toggled(widget)
# Custom URL handler
widget = guiget('custom_url_handler_entry')
widget.set_text(self.config['custom_url_handler'])
## Background tab
# Radio values
if self.config['background_type'] == 'solid':
guiget('solid_radiobutton').set_active(True)
elif self.config['background_type'] == 'transparent':
guiget('transparent_radiobutton').set_active(True)
self.update_background_tab()
# Background shading
widget = guiget('background_darkness_scale')
widget.set_value(float(self.config['background_darkness']))
## 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'])
# Scrollback infinite
widget = guiget('scrollback_infinite')
widget.set_active(self.config['scrollback_infinite'])
# 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(1)
elif value == 'ascii-del':
widget.set_active(2)
elif value == 'escape-sequence':
widget.set_active(3)
else:
widget.set_active(0)
# Delete key
widget = guiget('delete_binding_combobox')
value = self.config['delete_binding']
if value == 'control-h':
widget.set_active(1)
elif value == 'ascii-del':
widget.set_active(2)
elif value == 'escape-sequence':
widget.set_active(3)
else:
widget.set_active(0)
# Encoding
rowiter = None
widget = guiget('encoding_combobox')
encodingstore = guiget('EncodingListStore')
value = self.config['encoding']
encodings = TerminatorEncoding().get_list()
encodings.sort(lambda x, y: cmp(x[2].lower(), y[2].lower()))
for encoding in encodings:
if encoding[1] is None:
continue
label = "%s %s" % (encoding[2], encoding[1])
rowiter = encodingstore.append([label, encoding[1]])
if encoding[1] == value:
widget.set_active_iter(rowiter)
def set_layout(self, layout_name):
"""Set a layout"""
self.layouteditor.set_layout(layout_name)
def on_wingeomcheck_toggled(self, widget):
"""Window geometry setting changed"""
self.config['geometry_hinting'] = widget.get_active()
self.config.save()
def on_homogeneous_toggled(self, widget):
"""homogeneous_tabbar setting changed"""
guiget = self.builder.get_object
self.config['homogeneous_tabbar'] = widget.get_active()
scroll_toggled = guiget('scrolltabbarcheck')
if widget.get_active():
scroll_toggled.set_sensitive(True)
else:
scroll_toggled.set_active(True)
scroll_toggled.set_sensitive(False)
self.config.save()
def on_scroll_toggled(self, widget):
"""scroll_tabbar setting changed"""
self.config['scroll_tabbar'] = widget.get_active()
self.config.save()
def on_dbuscheck_toggled(self, widget):
"""DBus server setting changed"""
self.config['dbus'] = widget.get_active()
self.config.save()
def on_winbordercheck_toggled(self, widget):
"""Window border setting changed"""
self.config['borderless'] = not widget.get_active()
self.config.save()
def on_extrastylingcheck_toggled(self, widget):
"""Extra styling setting changed"""
self.config['extra_styling'] = widget.get_active()
self.config.save()
def on_hidefromtaskbcheck_toggled(self, widget):
"""Hide from taskbar setting changed"""
self.config['hide_from_taskbar'] = widget.get_active()
self.config.save()
def on_alwaysontopcheck_toggled(self, widget):
"""Always on top setting changed"""
self.config['always_on_top'] = widget.get_active()
self.config.save()
def on_hideonlosefocuscheck_toggled(self, widget):
"""Hide on lose focus setting changed"""
self.config['hide_on_lose_focus'] = widget.get_active()
self.config.save()
def on_stickycheck_toggled(self, widget):
"""Sticky setting changed"""
self.config['sticky'] = widget.get_active()
self.config.save()
def on_title_hide_sizetextcheck_toggled(self, widget):
"""Window geometry setting changed"""
self.config['title_hide_sizetext'] = widget.get_active()
self.config.save()
def on_always_split_with_profile_toggled(self, widget):
"""Always split with profile setting changed"""
self.config['always_split_with_profile'] = widget.get_active()
self.config.save()
def on_allow_bold_checkbutton_toggled(self, widget):
"""Allow bold setting changed"""
self.config['allow_bold'] = widget.get_active()
self.config.save()
def on_show_titlebar_toggled(self, widget):
"""Show titlebar setting changed"""
self.config['show_titlebar'] = widget.get_active()
self.config.save()
def on_copy_on_selection_toggled(self, widget):
"""Copy on selection setting changed"""
self.config['copy_on_selection'] = widget.get_active()
self.config.save()
def on_rewrap_on_resize_toggled(self, widget):
"""Rewrap on resize setting changed"""
self.config['rewrap_on_resize'] = widget.get_active()
self.config.save()
def on_putty_paste_style_toggled(self, widget):
"""Putty paste style setting changed"""
self.config['putty_paste_style'] = widget.get_active()
self.config.save()
def on_smart_copy_toggled(self, widget):
"""Putty paste style setting changed"""
self.config['smart_copy'] = widget.get_active()
self.config.save()
def on_cursor_blink_toggled(self, widget):
"""Cursor blink setting changed"""
self.config['cursor_blink'] = widget.get_active()
self.config.save()
def on_icon_bell_checkbutton_toggled(self, widget):
"""Icon bell setting changed"""
self.config['icon_bell'] = widget.get_active()
self.config.save()
def on_visual_bell_checkbutton_toggled(self, widget):
"""Visual bell setting changed"""
self.config['visible_bell'] = widget.get_active()
self.config.save()
def on_audible_bell_checkbutton_toggled(self, widget):
"""Audible bell setting changed"""
self.config['audible_bell'] = widget.get_active()
self.config.save()
def on_urgent_bell_checkbutton_toggled(self, widget):
"""Window manager bell setting changed"""
self.config['urgent_bell'] = widget.get_active()
self.config.save()
def on_login_shell_checkbutton_toggled(self, widget):
"""Login shell setting changed"""
self.config['login_shell'] = widget.get_active()
self.config.save()
def on_scroll_background_checkbutton_toggled(self, widget):
"""Scroll background setting changed"""
self.config['scroll_background'] = widget.get_active()
self.config.save()
def on_scroll_on_keystroke_checkbutton_toggled(self, widget):
"""Scroll on keystrong setting changed"""
self.config['scroll_on_keystroke'] = widget.get_active()
self.config.save()
def on_scroll_on_output_checkbutton_toggled(self, widget):
"""Scroll on output setting changed"""
self.config['scroll_on_output'] = widget.get_active()
self.config.save()
def on_delete_binding_combobox_changed(self, widget):
"""Delete binding setting changed"""
selected = widget.get_active()
if selected == 1:
value = 'control-h'
elif selected == 2:
value = 'ascii-del'
elif selected == 3:
value = 'escape-sequence'
else:
value = 'automatic'
self.config['delete_binding'] = value
self.config.save()
def on_backspace_binding_combobox_changed(self, widget):
"""Backspace binding setting changed"""
selected = widget.get_active()
if selected == 1:
value = 'control-h'
elif selected == 2:
value = 'ascii-del'
elif selected == 3:
value == 'escape-sequence'
else:
value = 'automatic'
self.config['backspace_binding'] = value
self.config.save()
def on_encoding_combobox_changed(self, widget):
"""Encoding setting changed"""
selected = widget.get_active_iter()
liststore = widget.get_model()
value = liststore.get_value(selected, 1)
self.config['encoding'] = value
self.config.save()
def on_scrollback_lines_spinbutton_value_changed(self, widget):
"""Scrollback lines setting changed"""
value = widget.get_value_as_int()
self.config['scrollback_lines'] = value
self.config.save()
def on_scrollback_infinite_toggled(self, widget):
"""Scrollback infiniteness changed"""
spinbutton = self.builder.get_object('scrollback_lines_spinbutton')
value = widget.get_active()
if value == True:
spinbutton.set_sensitive(False)
else:
spinbutton.set_sensitive(True)
self.config['scrollback_infinite'] = value
self.config.save()
def on_scrollbar_position_combobox_changed(self, widget):
"""Scrollbar position setting changed"""
selected = widget.get_active()
if selected == 1:
value = 'right'
elif selected == 2:
value = 'hidden'
else:
value = 'left'
self.config['scrollbar_position'] = value
self.config.save()
def on_darken_background_scale_value_changed(self, widget):
"""Background darkness setting changed"""
value = widget.get_value() # This one is rounded according to the UI.
if value > 1.0:
value = 1.0
self.config['background_darkness'] = value
self.config.save()
def on_palette_combobox_changed(self, widget):
"""Palette selector changed"""
value = None
guiget = self.builder.get_object
active = widget.get_active()
for key in self.palettevalues.keys():
if self.palettevalues[key] == active:
value = key
if value == 'custom':
sensitive = True
else:
sensitive = False
for num in xrange(1, 17):
picker = guiget('palette_colorpicker_%d' % num)
picker.set_sensitive(sensitive)
if value in self.palettes:
palette = self.palettes[value]
palettebits = palette.split(':')
for num in xrange(1, 17):
# Update the visible elements
picker = guiget('palette_colorpicker_%d' % num)
picker.set_color(Gdk.color_parse(palettebits[num - 1]))
elif value == 'custom':
palettebits = []
for num in xrange(1, 17):
picker = guiget('palette_colorpicker_%d' % num)
palettebits.append(color2hex(picker))
palette = ':'.join(palettebits)
else:
err('Unknown palette value: %s' % value)
return
self.config['palette'] = palette
self.config.save()
def on_background_colorpicker_color_set(self, widget):
"""Background color changed"""
self.config['background_color'] = color2hex(widget)
self.config.save()
def on_foreground_colorpicker_color_set(self, widget):
"""Foreground color changed"""
self.config['foreground_color'] = color2hex(widget)
self.config.save()
def on_palette_colorpicker_color_set(self, widget):
"""A palette colour changed"""
palette = None
palettebits = []
guiget = self.builder.get_object
# FIXME: We do this at least once elsewhere. refactor!
for num in xrange(1, 17):
picker = guiget('palette_colorpicker_%d' % num)
value = color2hex(picker)
palettebits.append(value)
palette = ':'.join(palettebits)
self.config['palette'] = palette
self.config.save()
def on_exit_action_combobox_changed(self, widget):
"""Exit action changed"""
selected = widget.get_active()
if selected == 1:
value = 'restart'
elif selected == 2:
value = 'hold'
else:
value = 'close'
self.config['exit_action'] = value
self.config.save()
def on_custom_url_handler_entry_changed(self, widget):
"""Custom URL handler value changed"""
self.config['custom_url_handler'] = widget.get_text()
self.config.save()
def on_custom_command_entry_changed(self, widget):
"""Custom command value changed"""
self.config['custom_command'] = widget.get_text()
self.config.save()
def on_cursor_color_type_toggled(self, widget):
guiget = self.builder.get_object
customwidget = guiget('cursor_color_custom_radiobutton')
colorwidget = guiget('cursor_color')
colorwidget.set_sensitive(customwidget.get_active())
self.config['cursor_color_fg'] = not customwidget.get_active()
try:
colorwidget.set_color(Gdk.color_parse(self.config['cursor_color']))
except (ValueError, TypeError):
try:
self.config['cursor_color'] = self.config['foreground_color']
colorwidget.set_color(Gdk.color_parse(self.config['cursor_color']))
except ValueError:
self.config['cursor_color'] = "#FFFFFF"
colorwidget.set_color(Gdk.color_parse(self.config['cursor_color']))
self.config.save()
def on_cursor_color_color_set(self, widget):
"""Cursor colour changed"""
self.config['cursor_color'] = color2hex(widget)
self.config.save()
def on_cursor_shape_combobox_changed(self, widget):
"""Cursor shape changed"""
selected = widget.get_active()
if selected == 1:
value = 'underline'
elif selected == 2:
value = 'ibeam'
else:
value = 'block'
self.config['cursor_shape'] = value
self.config.save()
def on_word_chars_entry_changed(self, widget):
"""Word characters changed"""
self.config['word_chars'] = widget.get_text()
self.config.save()
def on_font_selector_font_set(self, widget):
"""Font changed"""
self.config['font'] = widget.get_font_name()
self.config.save()
def on_title_font_selector_font_set(self, widget):
"""Titlebar Font changed"""
self.config['title_font'] = widget.get_font_name()
self.config.save()
def on_title_receive_bg_color_color_set(self, widget):
"""Title receive background colour changed"""
self.config['title_receive_bg_color'] = color2hex(widget)
self.config.save()
def on_title_receive_fg_color_color_set(self, widget):
"""Title receive foreground colour changed"""
self.config['title_receive_fg_color'] = color2hex(widget)
self.config.save()
def on_title_inactive_bg_color_color_set(self, widget):
"""Title inactive background colour changed"""
self.config['title_inactive_bg_color'] = color2hex(widget)
self.config.save()
def on_title_transmit_bg_color_color_set(self, widget):
"""Title transmit backgruond colour changed"""
self.config['title_transmit_bg_color'] = color2hex(widget)
self.config.save()
def on_title_inactive_fg_color_color_set(self, widget):
"""Title inactive foreground colour changed"""
self.config['title_inactive_fg_color'] = color2hex(widget)
self.config.save()
def on_title_transmit_fg_color_color_set(self, widget):
"""Title transmit foreground colour changed"""
self.config['title_transmit_fg_color'] = color2hex(widget)
self.config.save()
def on_inactive_color_offset_value_changed(self, widget):
"""Inactive color offset setting changed"""
value = widget.get_value() # This one is rounded according to the UI.
if value > 1.0:
value = 1.0
self.config['inactive_color_offset'] = value
self.config.save()
guiget = self.builder.get_object
label_widget = guiget('inactive_color_offset_value_label')
label_widget.set_text('%d%%' % (int(value * 100)))
def on_handlesize_value_changed(self, widget):
"""Handle size changed"""
value = widget.get_value() # This one is rounded according to the UI.
value = int(value) # Cast to int.
if value > 20:
value = 20
self.config['handle_size'] = value
self.config.save()
guiget = self.builder.get_object
label_widget = guiget('handlesize_value_label')
label_widget.set_text(str(value))
def on_focuscombo_changed(self, widget):
"""Focus type changed"""
selected = widget.get_active()
if selected == 1:
value = 'click'
elif selected == 2:
value = 'mouse'
else:
value = 'system'
self.config['focus'] = value
self.config.save()
def on_tabposcombo_changed(self, widget):
"""Tab position changed"""
selected = widget.get_active()
if selected == 1:
value = 'bottom'
elif selected == 2:
value = 'left'
elif selected == 3:
value = 'right'
elif selected == 4:
value = 'hidden'
else:
value = 'top'
self.config['tab_position'] = value
self.config.save()
def on_broadcastdefault_changed(self, widget):
"""Broadcast default changed"""
selected = widget.get_active()
if selected == 0:
value = 'all'
elif selected == 2:
value = 'off'
else:
value = 'group'
self.config['broadcast_default'] = value
self.config.save()
def on_winstatecombo_changed(self, widget):
"""Window state changed"""
selected = widget.get_active()
if selected == 1:
value = 'hidden'
elif selected == 2:
value = 'maximise'
elif selected == 3:
value = 'fullscreen'
else:
value = 'normal'
self.config['window_state'] = value
self.config.save()
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):
res = model.append([newprofile, True])
if res:
path = model.get_path(res)
treeview.set_cursor(path, column=treeview.get_column(0),
start_editing=True)
self.layouteditor.update_profiles()
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_profile_selection = None
self.config.del_profile(profile)
model.remove(rowiter)
selection.select_iter(model.get_iter_first())
self.layouteditor.update_profiles()
def on_layoutaddbutton_clicked(self, _button):
"""Add a new layout to the list"""
terminator = Terminator()
current_layout = terminator.describe_layout()
guiget = self.builder.get_object
treeview = guiget('layoutlist')
model = treeview.get_model()
values = [ r[0] for r in model ]
name = _('New Layout')
if name in values:
i = 0
while name in values:
i = i + 1
name = '%s %d' % (_('New Layout'), i)
if self.config.add_layout(name, current_layout):
res = model.append([name, True])
if res:
path = model.get_path(res)
treeview.set_cursor(path, start_editing=True)
self.config.save()
def on_layoutrefreshbutton_clicked(self, _button):
"""Refresh the terminals status and update"""
terminator = Terminator()
current_layout = terminator.describe_layout()
guiget = self.builder.get_object
treeview = guiget('layoutlist')
selected = treeview.get_selection()
(model, rowiter) = selected.get_selected()
name = model.get_value(rowiter, 0)
if self.config.replace_layout(name, current_layout):
treeview.set_cursor(model.get_path(rowiter), column=treeview.get_column(0), start_editing=False)
self.config.save()
def on_layoutremovebutton_clicked(self, _button):
"""Remove a layout from the list"""
guiget = self.builder.get_object
treeview = guiget('layoutlist')
selection = treeview.get_selection()
(model, rowiter) = selection.get_selected()
layout = model.get_value(rowiter, 0)
if layout == 'default':
# We shouldn't let people delete this layout
return
self.previous_selection = None
self.config.del_layout(layout)
model.remove(rowiter)
selection.select_iter(model.get_iter_first())
self.config.save()
def on_use_custom_url_handler_checkbutton_toggled(self, checkbox):
"""Toggling the use_custom_url_handler checkbox needs to alter the
sensitivity of the custom_url_handler entrybox"""
guiget = self.builder.get_object
widget = guiget('custom_url_handler_entry')
value = checkbox.get_active()
widget.set_sensitive(value)
self.config['use_custom_url_handler'] = value
self.config.save()
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')
value = checkbox.get_active()
widget.set_sensitive(value)
self.config['use_custom_command'] = value
self.config.save()
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')
value = checkbox.get_active()
widget.set_sensitive(not value)
self.config['use_system_font'] = value
self.config.save()
if self.config['use_system_font'] == True:
fontname = self.config.get_system_mono_font()
if fontname is not None:
widget.set_font_name(fontname)
else:
widget.set_font_name(self.config['font'])
def on_title_system_font_checkbutton_toggled(self, checkbox):
"""Toggling the title_use_system_font checkbox needs to alter the
sensitivity of the font selector"""
guiget = self.builder.get_object
widget = guiget('title_font_selector')
value = checkbox.get_active()
widget.set_sensitive(not value)
self.config['title_use_system_font'] = value
self.config.save()
if self.config['title_use_system_font'] == True:
fontname = self.config.get_system_prop_font()
if fontname is not None:
widget.set_font_name(fontname)
else:
widget.set_font_name(self.config['title_font'])
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(2)
widget = guiget('delete_binding_combobox')
widget.set_active(3)
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
imagewidget = guiget('image_radiobutton')
transwidget = guiget('transparent_radiobutton')
if transwidget.get_active() == True:
backtype = 'transparent'
else:
backtype = 'solid'
self.config['background_type'] = backtype
self.config.save()
if backtype in ('transparent', 'image'):
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"""
(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_profile_selection = profile
widget = self.builder.get_object('profileremovebutton')
if profile == 'default':
widget.set_sensitive(False)
else:
widget.set_sensitive(True)
def on_plugin_selection_changed(self, selection):
"""A different plugin was selected"""
(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
plugin = listmodel.get_value(rowiter, 0)
self.set_plugin(plugin)
self.previous_plugin_selection = plugin
widget = self.builder.get_object('plugintogglebutton')
def on_plugin_toggled(self, cell, path):
"""A plugin has been enabled or disabled"""
treeview = self.builder.get_object('pluginlist')
model = treeview.get_model()
plugin = model[path][0]
if not self.plugins[plugin]:
# Plugin is currently disabled, load it
self.registry.enable(plugin)
else:
# Plugin is currently enabled, unload it
self.registry.disable(plugin)
self.plugins[plugin] = not self.plugins[plugin]
# Update the treeview
model[path][1] = self.plugins[plugin]
enabled_plugins = [x for x in self.plugins if self.plugins[x] == True]
self.config['enabled_plugins'] = enabled_plugins
self.config.save()
def set_plugin(self, plugin):
"""Show the preferences for the selected plugin, if any"""
pluginpanelabel = self.builder.get_object('pluginpanelabel')
pluginconfig = self.config.plugin_get_config(plugin)
# FIXME: Implement this, we need to auto-construct a UI for the plugin
def on_profile_name_edited(self, cell, path, newtext):
"""Update a profile name"""
oldname = cell.get_property('text')
if oldname == newtext or oldname == 'default':
return
dbg('PrefsEditor::on_profile_name_edited: Changing %s to %s' %
(oldname, newtext))
self.config.rename_profile(oldname, newtext)
self.config.save()
widget = self.builder.get_object('profilelist')
model = widget.get_model()
itera = model.get_iter(path)
model.set_value(itera, 0, newtext)
if oldname == self.previous_profile_selection:
self.previous_profile_selection = newtext
def on_layout_selection_changed(self, selection):
"""A different layout was selected"""
self.layouteditor.on_layout_selection_changed(selection)
def on_layout_item_selection_changed(self, selection):
"""A different item in the layout was selected"""
self.layouteditor.on_layout_item_selection_changed(selection)
def on_layout_profile_chooser_changed(self, widget):
"""A different profile has been selected for this item"""
self.layouteditor.on_layout_profile_chooser_changed(widget)
def on_layout_profile_command_changed(self, widget):
"""A different command has been entered for this item"""
self.layouteditor.on_layout_profile_command_activate(widget)
def on_layout_profile_workingdir_changed(self, widget):
"""A different working directory has been entered for this item"""
self.layouteditor.on_layout_profile_workingdir_activate(widget)
def on_layout_name_edited(self, cell, path, newtext):
"""Update a layout name"""
oldname = cell.get_property('text')
if oldname == newtext or oldname == 'default':
return
dbg('Changing %s to %s' % (oldname, newtext))
self.config.rename_layout(oldname, newtext)
self.config.save()
widget = self.builder.get_object('layoutlist')
model = widget.get_model()
itera = model.get_iter(path)
model.set_value(itera, 0, newtext)
if oldname == self.previous_layout_selection:
self.previous_layout_selection = newtext
if oldname == self.layouteditor.layout_name:
self.layouteditor.layout_name = 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 in self.colourschemes:
forecol = self.colourschemes[value][0]
backcol = self.colourschemes[value][1]
elif value == 'custom':
forecol = color2hex(fore)
backcol = color2hex(back)
else:
err('Unknown colourscheme value: %s' % value)
return
fore.set_color(Gdk.color_parse(forecol))
back.set_color(Gdk.color_parse(backcol))
self.config['foreground_color'] = forecol
self.config['background_color'] = backcol
self.config.save()
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)
self.config['use_theme_colors'] = active
self.config.save()
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)
binding = liststore.get_value(liststore.get_iter(path), 0)
accel = Gtk.accelerator_name(key, mods)
self.config['keybindings'][binding] = accel
self.config.save()
def on_cellrenderer_accel_cleared(self, liststore, path):
"""Handle the clearing of a keybinding accelerator"""
celliter = liststore.get_iter_from_string(path)
liststore.set(celliter, 2, 0, 3, 0)
binding = liststore.get_value(liststore.get_iter(path), 0)
self.config['keybindings'][binding] = None
self.config.save()
def on_open_manual(self, widget):
"""Open the fine manual"""
self.term.key_help()
class LayoutEditor:
profile_ids_to_profile = None
profile_profile_to_ids = None
layout_name = None
layout_item = None
builder = None
treeview = None
treestore = None
config = None
def __init__(self, builder):
"""Initialise ourself"""
self.config = config.Config()
self.builder = builder
def prepare(self, layout=None):
"""Do the things we can't do in __init__"""
self.treeview = self.builder.get_object('LayoutTreeView')
self.treestore = self.builder.get_object('LayoutTreeStore')
self.update_profiles()
if layout:
self.set_layout(layout)
def set_layout(self, layout_name):
"""Load a particular layout"""
self.layout_name = layout_name
store = self.treestore
layout = self.config.layout_get_config(layout_name)
listitems = {}
store.clear()
children = layout.keys()
i = 0
while children != []:
child = children.pop()
child_type = layout[child]['type']
parent = layout[child]['parent']
if child_type != 'Window' and parent not in layout:
# We have an orphan!
err('%s is an orphan in this layout. Discarding' % child)
continue
try:
parentiter = listitems[parent]
except KeyError:
if child_type == 'Window':
parentiter = None
else:
# We're not ready for this widget yet
children.insert(0, child)
continue
if child_type == 'VPaned':
child_type = 'Vertical split'
elif child_type == 'HPaned':
child_type = 'Horizontal split'
listitems[child] = store.append(parentiter, [child, child_type])
treeview = self.builder.get_object('LayoutTreeView')
treeview.expand_all()
def update_profiles(self):
"""Update the list of profiles"""
self.profile_ids_to_profile = {}
self.profile_profile_to_ids= {}
chooser = self.builder.get_object('layout_profile_chooser')
profiles = self.config.list_profiles()
profiles.sort()
i = 0
for profile in profiles:
self.profile_ids_to_profile[i] = profile
self.profile_profile_to_ids[profile] = i
chooser.append_text(profile)
i = i + 1
def on_layout_selection_changed(self, selection):
"""A different layout was selected"""
(listmodel, rowiter) = selection.get_selected()
if not rowiter:
# Something is wrong, just jump to the first item in the list
selection.select_iter(self.treestore.get_iter_first())
return
layout = listmodel.get_value(rowiter, 0)
self.set_layout(layout)
self.previous_layout_selection = layout
widget = self.builder.get_object('layoutremovebutton')
if layout == 'default':
widget.set_sensitive(False)
else:
widget.set_sensitive(True)
command = self.builder.get_object('layout_profile_command')
chooser = self.builder.get_object('layout_profile_chooser')
workdir = self.builder.get_object('layout_profile_workingdir')
command.set_sensitive(False)
chooser.set_sensitive(False)
workdir.set_sensitive(False)
def on_layout_item_selection_changed(self, selection):
"""A different item in the layout was selected"""
(treemodel, rowiter) = selection.get_selected()
if not rowiter:
return
item = treemodel.get_value(rowiter, 0)
self.layout_item = item
self.set_layout_item(item)
def set_layout_item(self, item_name):
"""Set a layout item"""
layout = self.config.layout_get_config(self.layout_name)
layout_item = layout[self.layout_item]
command = self.builder.get_object('layout_profile_command')
chooser = self.builder.get_object('layout_profile_chooser')
workdir = self.builder.get_object('layout_profile_workingdir')
if layout_item['type'] != 'Terminal':
command.set_sensitive(False)
chooser.set_sensitive(False)
workdir.set_sensitive(False)
return
command.set_sensitive(True)
chooser.set_sensitive(True)
workdir.set_sensitive(True)
if layout_item.has_key('command') and layout_item['command'] != '':
command.set_text(layout_item['command'])
else:
command.set_text('')
if layout_item.has_key('profile') and layout_item['profile'] != '':
chooser.set_active(self.profile_profile_to_ids[layout_item['profile']])
else:
chooser.set_active(0)
if layout_item.has_key('directory') and layout_item['directory'] != '':
workdir.set_text(layout_item['directory'])
else:
workdir.set_text('')
def on_layout_profile_chooser_changed(self, widget):
"""A new profile has been selected for this item"""
if not self.layout_item:
return
profile = widget.get_active_text()
layout = self.config.layout_get_config(self.layout_name)
layout[self.layout_item]['profile'] = profile
self.config.save()
def on_layout_profile_command_activate(self, widget):
"""A new command has been entered for this item"""
command = widget.get_text()
layout = self.config.layout_get_config(self.layout_name)
layout[self.layout_item]['command'] = command
self.config.save()
def on_layout_profile_workingdir_activate(self, widget):
"""A new working directory has been entered for this item"""
workdir = widget.get_text()
layout = self.config.layout_get_config(self.layout_name)
layout[self.layout_item]['directory'] = workdir
self.config.save()
if __name__ == '__main__':
import util
util.DEBUG = True
import terminal
TERM = terminal.Terminal()
PREFEDIT = PrefsEditor(TERM)
Gtk.main()
terminator-1.91/terminatorlib/util.py 0000775 0001750 0001750 00000027010 13054612071 020334 0 ustar steve steve 0000000 0000000 #!/usr/bin/env python2
# 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"""
import sys
import cairo
import os
import pwd
import inspect
import uuid
import subprocess
import gi
try:
gi.require_version('Gtk','3.0')
from gi.repository import Gtk, Gdk
except ImportError:
print('You need Gtk 3.0+ to run Remotinator.')
sys.exit(1)
# set this to true to enable debugging output
DEBUG = False
# set this to true to additionally list filenames in debugging
DEBUGFILES = False
# list of classes to show debugging for. empty list means show all classes
DEBUGCLASSES = []
# list of methods to show debugging for. empty list means show all methods
DEBUGMETHODS = []
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 = ""
if DEBUGCLASSES != [] and classname not in DEBUGCLASSES:
return
if DEBUGMETHODS != [] and method not in DEBUGMETHODS:
return
try:
print >> sys.stderr, "%s::%s: %s%s" % (classname, method, log, extra)
except IOError:
pass
def err(log = ""):
"""Print an error message"""
try:
print >> sys.stderr, log
except IOError:
pass
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.DialogFlags.MODAL,
Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, message)
dialog.run()
dialog.destroy()
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 manual_lookup():
'''Choose the manual to open based on LANGUAGE'''
available_languages = ['en']
base_url = 'http://terminator-gtk3.readthedocs.io/%s/latest/'
target = 'en' # default to English
if 'LANGUAGE' in os.environ:
languages = os.environ['LANGUAGE'].split(':')
for language in languages:
if language in available_languages:
target = language
break
return base_url % target
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"""
try:
usershell = pwd.getpwuid(os.getuid())[6]
except KeyError:
usershell = None
shells = [usershell, '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"""
# FIXME: Can this be changed from using "import cairo" to "from gi.repository import cairo"?
window = widget.get_window()
width, height = window.get_width(), window.get_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
preview_width, preview_height = int(width * factor), int(height * factor)
preview_surface = Gdk.Window.create_similar_surface(window,
cairo.CONTENT_COLOR, preview_width, preview_height)
cairo_context = cairo.Context(preview_surface)
cairo_context.scale(factor, factor)
Gdk.cairo_set_source_window(cairo_context, window, 0, 0)
cairo_context.paint()
scaledpixbuf = Gdk.pixbuf_get_from_surface(preview_surface, 0, 0, preview_width, preview_height);
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')
dbg('Found config dir: %s' % configdir)
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
>>> a = {'foo': 'bar', 'baz': 'bjonk'}
>>> b = {'foo': 'far', 'baz': 'bjonk'}
>>> dict_diff(a, b)
{'foo': 'far'}
"""
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
p1, p2 = allocation.y, allocation.y + allocation.height
elif direction == 'up':
edge = allocation.y
p1, p2 = allocation.x, allocation.x + allocation.width
elif direction == 'right':
edge = allocation.x + allocation.width
p1, p2 = allocation.y, allocation.y + allocation.height
elif direction == 'down':
edge = allocation.y + allocation.height
p1, p2 = allocation.x, allocation.x + allocation.width
else:
raise ValueError('unknown direction %s' % direction)
return(edge, p1, p2)
def get_nav_possible(edge, allocation, direction, p1, p2):
"""Check if the supplied allocation is in the right direction of the
supplied edge"""
x1, x2 = allocation.x, allocation.x + allocation.width
y1, y2 = allocation.y, allocation.y + allocation.height
if direction == 'left':
return(x2 <= edge and y1 <= p2 and y2 >= p1)
elif direction == 'right':
return(x1 >= edge and y1 <= p2 and y2 >= p1)
elif direction == 'up':
return(y2 <= edge and x1 <= p2 and x2 >= p1)
elif direction == 'down':
return(y1 >= edge and x1 <= p2 and x2 >= p1)
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(allocation.x - edge)
elif direction == 'up':
return(edge - (allocation.y + allocation.height))
elif direction == 'down':
return(allocation.y - edge)
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)
def enumerate_descendants(parent):
"""Walk all our children and build up a list of containers and
terminals"""
# FIXME: Does having to import this here mean we should move this function
# back to Container?
from factory import Factory
containerstmp = []
containers = []
terminals = []
maker = Factory()
if parent is None:
err('no parent widget specified')
return
for descendant in parent.get_children():
if maker.isinstance(descendant, 'Container'):
containerstmp.append(descendant)
elif maker.isinstance(descendant, 'Terminal'):
terminals.append(descendant)
while len(containerstmp) > 0:
child = containerstmp.pop(0)
for descendant in child.get_children():
if maker.isinstance(descendant, 'Container'):
containerstmp.append(descendant)
elif maker.isinstance(descendant, 'Terminal'):
terminals.append(descendant)
containers.append(child)
dbg('%d containers and %d terminals fall beneath %s' % (len(containers),
len(terminals), parent))
return(containers, terminals)
def make_uuid(str_uuid=None):
"""Generate a UUID for an object"""
if str_uuid:
return uuid.UUID(str_uuid)
return uuid.uuid4()
def inject_uuid(target):
"""Inject a UUID into an existing object"""
uuid = make_uuid()
if not hasattr(target, "uuid") or target.uuid == None:
dbg("Injecting UUID %s into: %s" % (uuid, target))
target.uuid = uuid
else:
dbg("Object already has a UUID: %s" % target)
def spawn_new_terminator(cwd, args):
"""Start a new terminator instance with the given arguments"""
cmd = sys.argv[0]
if not os.path.isabs(cmd):
# Command is not an absolute path. Figure out where we are
cmd = os.path.join (cwd, sys.argv[0])
if not os.path.isfile(cmd):
# we weren't started as ./terminator in a path. Give up
err('Unable to locate Terminator')
return False
dbg("Spawning: %s" % cmd)
subprocess.Popen([cmd]+args)
def display_manager():
"""Try to detect which display manager we run under"""
if os.environ.get('WAYLAND_DISPLAY'):
return 'WAYLAND'
# Fallback assumption of X11
return 'X11'
terminator-1.91/terminatorlib/container.py 0000775 0001750 0001750 00000026410 13054612071 021344 0 ustar steve steve 0000000 0000000 #!/usr/bin/env python2
# Terminator by Chris Jones
# GPL v2 only
"""container.py - classes necessary to contain Terminal widgets"""
from gi.repository import GObject
from gi.repository 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 direct child widgets, if any"""
return(self.children)
def get_child_metadata(self, widget):
"""Return metadata that would be useful to recreate ourselves after our
child is .remove()d and .add()ed"""
return None
def split_horiz(self, widget, cwd=None):
"""Split this container horizontally"""
return(self.split_axis(widget, True, cwd))
def split_vert(self, widget, cwd=None):
"""Split this container vertically"""
return(self.split_axis(widget, False, cwd))
def split_axis(self, widget, vertical=True, cwd=None, sibling=None, siblinglast=None):
"""Default axis splitter. This should be implemented by subclasses"""
raise NotImplementedError('split_axis')
def rotate(self, widget, clockwise):
"""Rotate children in this container"""
raise NotImplementedError('rotate')
def add(self, widget, metadata=None):
"""Add a widget to the container"""
raise NotImplementedError('add')
def remove(self, widget):
"""Remove a widget from the container"""
raise NotImplementedError('remove')
def replace(self, oldwidget, newwidget):
"""Replace the child oldwidget with newwidget. This is the bare minimum
required for this operation. Containers should override it if they have
more complex requirements"""
if not oldwidget in self.get_children():
err('%s is not a child of %s' % (oldwidget, self))
return
self.remove(oldwidget)
self.add(newwidget)
def hoover(self):
"""Ensure we still have a reason to exist"""
raise NotImplementedError('hoover')
def get_children(self):
"""Return an ordered list of the children of this Container"""
raise NotImplementedError('get_children')
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)
widget.close()
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"""
# skip this dialog if applicable
if self.config['suppress_multiple_term_dialog']:
return Gtk.ResponseType.ACCEPT
dialog = Gtk.Dialog(_('Close?'), window, Gtk.DialogFlags.MODAL)
dialog.set_resizable(False)
dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT)
c_all = dialog.add_button(Gtk.STOCK_CLOSE, Gtk.ResponseType.ACCEPT)
c_all.get_children()[0].get_children()[0].get_children()[1].set_label(
_('Close _Terminals'))
primary = Gtk.Label(label=_('Close multiple terminals?'))
primary.set_use_markup(True)
primary.set_alignment(0, 0.5)
if reqtype == 'window':
label_text = _('This window has several terminals open. Closing \
the window will also close all terminals within it.')
elif reqtype == 'tab':
label_text = _('This tab has several terminals open. Closing \
the tab will also close all terminals within it.')
else:
label_text = ''
secondary = Gtk.Label(label=label_text)
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.IconSize.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)
checkbox = Gtk.CheckButton(_("Do not show this message next time"))
dialog.vbox.pack_end(checkbox, True, True, 0)
dialog.show_all()
result = dialog.run()
# set configuration
self.config.base.reload()
self.config['suppress_multiple_term_dialog'] = checkbox.get_active()
self.config.save()
dialog.destroy()
return(result)
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"""
if not hasattr(self, 'cached_maker'):
self.cached_maker = Factory()
maker = self.cached_maker
terminals = {}
for child in self.get_offspring():
if not child:
continue
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)
def describe_layout(self, count, parent, global_layout, child_order):
"""Describe our current layout"""
layout = {}
maker = Factory()
mytype = maker.type(self)
if not mytype:
err('unable to detemine own type. %s' % self)
return({})
layout['type'] = mytype
layout['parent'] = parent
layout['order'] = child_order
if hasattr(self, 'get_position'):
position = self.get_position()
if hasattr(position, '__iter__'):
position = ':'.join([str(x) for x in position])
layout['position'] = position
if hasattr(self, 'ismaximised'):
layout['maximised'] = self.ismaximised
if hasattr(self, 'isfullscreen'):
layout['fullscreen'] = self.isfullscreen
if hasattr(self, 'ratio'):
layout['ratio'] = self.ratio
if hasattr(self, 'get_size'):
layout['size'] = self.get_size()
if hasattr(self, 'title'):
layout['title'] = self.title.text
if mytype == 'Notebook':
labels = []
last_active_term = []
for tabnum in xrange(0, self.get_n_pages()):
page = self.get_nth_page(tabnum)
label = self.get_tab_label(page)
labels.append(label.get_custom_label())
last_active_term.append(self.last_active_term[self.get_nth_page(tabnum)])
layout['labels'] = labels
layout['last_active_term'] = last_active_term
layout['active_page'] = self.get_current_page()
else:
if hasattr(self, 'last_active_term') and self.last_active_term is not None:
layout['last_active_term'] = self.last_active_term
if mytype == 'Window':
if self.uuid == self.terminator.last_active_window:
layout['last_active_window'] = True
else:
layout['last_active_window'] = False
name = 'child%d' % count
count = count + 1
global_layout[name] = layout
child_order = 0
for child in self.get_children():
if hasattr(child, 'describe_layout'):
count = child.describe_layout(count, name, global_layout, child_order)
child_order = child_order + 1
return(count)
def create_layout(self, layout):
"""Apply settings for our layout"""
raise NotImplementedError('create_layout')
# vim: set expandtab ts=4 sw=4:
terminator-1.91/terminatorlib/layoutlauncher.py 0000775 0001750 0001750 00000007307 13054612071 022425 0 ustar steve steve 0000000 0000000 #!/usr/bin/env python2
# Terminator by Chris Jones
# GPL v2 only
"""layoutlauncher.py - class for the Layout Launcher window"""
import os
from gi.repository import Gtk
from gi.repository import GObject
from util import dbg, err, spawn_new_terminator
import config
from translation import _
from terminator import Terminator
from plugin import PluginRegistry
class LayoutLauncher:
"""Class implementing the various parts of the preferences editor"""
terminator = None
config = None
registry = None
plugins = None
keybindings = None
window = None
builder = None
layouttreeview = None
layouttreestore = None
def __init__ (self):
self.terminator = Terminator()
self.terminator.register_launcher_window(self)
self.config = config.Config()
self.config.base.reload()
self.builder = Gtk.Builder()
try:
# Figure out where our library is on-disk so we can open our UI
(head, _tail) = os.path.split(config.__file__)
librarypath = os.path.join(head, 'layoutlauncher.glade')
gladefile = open(librarypath, 'r')
gladedata = gladefile.read()
except Exception, ex:
print "Failed to find layoutlauncher.glade"
print ex
return
self.builder.add_from_string(gladedata)
self.window = self.builder.get_object('layoutlauncherwin')
icon_theme = Gtk.IconTheme.get_default()
if icon_theme.lookup_icon('terminator-layout', 48, 0):
self.window.set_icon_name('terminator-layout')
else:
dbg('Unable to load Terminator layout launcher icon')
icon = self.window.render_icon(Gtk.STOCK_DIALOG_INFO, Gtk.IconSize.BUTTON)
self.window.set_icon(icon)
self.builder.connect_signals(self)
self.window.connect('destroy', self.on_destroy_event)
self.window.show_all()
self.layouttreeview = self.builder.get_object('layoutlist')
self.layouttreestore = self.builder.get_object('layoutstore')
self.update_layouts()
def on_destroy_event(self, widget, data=None):
"""Handle window destruction"""
dbg('destroying self')
self.terminator.deregister_launcher_window(self)
self.window.destroy()
del(self.window)
def update_layouts(self):
"""Update the contents of the layout"""
self.layouttreestore.clear()
layouts = self.config.list_layouts()
for layout in sorted(layouts, cmp=lambda x,y: cmp(x.lower(), y.lower())):
if layout != "default":
self.layouttreestore.append([layout])
else:
self.layouttreestore.prepend([layout])
def on_launchbutton_clicked(self, widget):
"""Handle button click"""
self.launch_layout()
def on_row_activated(self, widget, path, view_column):
"""Handle item double-click and return"""
self.launch_layout()
def launch_layout(self):
"""Launch the selected layout as new instance"""
dbg('We have takeoff!')
selection=self.layouttreeview.get_selection()
(listmodel, rowiter) = selection.get_selected()
if not rowiter:
# Something is wrong, just jump to the first item in the list
selection.select_iter(self.layouttreestore.get_iter_first())
(listmodel, rowiter) = selection.get_selected()
layout = listmodel.get_value(rowiter, 0)
dbg('Clicked for %s' % layout)
spawn_new_terminator(self.terminator.origcwd, ['-u', '-l', layout])
if __name__ == '__main__':
import util
util.DEBUG = True
import terminal
LAYOUTLAUNCHER = LayoutLauncher()
Gtk.main()
terminator-1.91/terminatorlib/freebsd.py 0000664 0001750 0001750 00000005573 13054612071 021000 0 ustar steve steve 0000000 0000000 #!/usr/bin/env python2
#
# Copyright (c) 2008, Thomas Hurst
#
# 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.
"""
freebsd.get_process_cwd(pid):
Use sysctl() to retrieve the cwd of an arbitrary process on FreeBSD
using kern.proc.filedesc, as used by procstat(1).
Tested on FreeBSD 7-STABLE/amd64 from April 11 2008.
"""
from ctypes import *
from ctypes.util import find_library
class sockaddr_storage(Structure):
"""struct sockaddr_storage, defined in /usr/include/sys/socket.h"""
_fields_ = [
('ss_len', c_char),
('ss_family', c_char), # /usr/include/sys/_types.h; _uint8_t
('__ss_pad1', c_char * 6), # (sizeof(int64) - sizeof(char) - sizeof(ss_family_t))
('__ss_align', c_longlong),
('__ss_pad2', c_char * 112), # (128(maxsize) - sizeof(char) - sizeof(ss_family_t) -
# sizeof(ss_pad1) - sizeof(int64))
]
class kinfo_file(Structure):
"""struct kinfo_file, defined in /usr/include/sys/user.h """
_fields_ = [
('kf_structsize', c_int),
('kf_type', c_int),
('kf_fd', c_int),
('kf_ref_count', c_int),
('kf_flags', c_int),
('kf_offset', c_size_t), # this is a off_t, a pointer
('kf_vnode_type', c_int),
('kf_sock_domain', c_int),
('kf_sock_type', c_int),
('kf_sock_protocol', c_int),
('kf_path', c_char * 1024), # PATH_MAX
('kf_sa_local', sockaddr_storage),
('kf_sa_peer', sockaddr_storage),
]
libc = CDLL(find_library('c'))
uintlen = c_size_t(sizeof(c_uint))
ver = c_uint(0)
if (libc.sysctlbyname('kern.osreldate', byref(ver), byref(uintlen), None, 0) < 0):
raise OSError, "sysctlbyname returned < 0"
# kern.proc.filedesc added for procstat(1) after these __FreeBSD_versions
if ver.value < 700104 and ver.value < 800019:
raise NotImplementedError, "cwd detection requires a recent 7.0-STABLE or 8-CURRENT"
def get_process_cwd(pid):
"""Return string containing the current working directory of the given pid,
or None on failure."""
# /usr/include/sys/sysctl.h
# CTL_KERN, KERN_PROC, KERN_PROC_FILEDESC
oid = (c_uint * 4)(1, 14, 14, pid)
if libc.sysctl(oid, 4, None, byref(uintlen), None, 0) < 0:
return None
buf = c_char_p(" " * uintlen.value)
if libc.sysctl(oid, 4, buf, byref(uintlen), None, 0) < 0:
return None
kifs = cast(buf, POINTER(kinfo_file))
for i in xrange(0, uintlen.value / sizeof(kinfo_file)):
kif = kifs[i]
if kif.kf_fd == -1: # KF_FD_TYPE_CWD
return kif.kf_path
if __name__ == '__main__':
import os, sys
print " => %d cwd = %s" % (os.getpid(), get_process_cwd(os.getpid()))
for pid in sys.argv:
try:
pid = int(pid)
except:
pass
else:
print " => %d cwd = %s" % (pid, get_process_cwd(pid))
terminator-1.91/terminatorlib/terminator.py 0000775 0001750 0001750 00000065665 13055372232 021570 0 ustar steve steve 0000000 0000000 #!/usr/bin/env python2
# Terminator by Chris Jones
# GPL v2 only
"""terminator.py - class for the master Terminator singleton"""
import copy
import os
import gi
gi.require_version('Vte', '2.91')
from gi.repository import Gtk, Gdk, Vte, GdkX11
from gi.repository.GLib import GError
import borg
from borg import Borg
from config import Config
from keybindings import Keybindings
from util import dbg, err, enumerate_descendants
from factory import Factory
from cwd import get_pid_cwd
from version import APP_NAME, APP_VERSION
def eventkey2gdkevent(eventkey): # FIXME FOR GTK3: is there a simpler way of casting from specific EventKey to generic (union) GdkEvent?
gdkevent = Gdk.Event.new(eventkey.type)
gdkevent.key.window = eventkey.window
gdkevent.key.send_event = eventkey.send_event
gdkevent.key.time = eventkey.time
gdkevent.key.state = eventkey.state
gdkevent.key.keyval = eventkey.keyval
gdkevent.key.length = eventkey.length
gdkevent.key.string = eventkey.string
gdkevent.key.hardware_keycode = eventkey.hardware_keycode
gdkevent.key.group = eventkey.group
gdkevent.key.is_modifier = eventkey.is_modifier
return gdkevent
class Terminator(Borg):
"""master object for the application"""
windows = None
launcher_windows = None
windowtitle = None
terminals = None
groups = None
config = None
keybindings = None
style_providers = None
last_focused_term = None
origcwd = None
dbus_path = None
dbus_name = None
pid_cwd = None
gnome_client = None
debug_address = None
ibus_running = None
doing_layout = None
layoutname = None
last_active_window = None
prelayout_windows = None
groupsend = None
groupsend_type = {'all':0, 'group':1, 'off':2}
cur_gtk_theme_name = None
gtk_settings = None
def __init__(self):
"""Class initialiser"""
Borg.__init__(self, self.__class__.__name__)
self.prepare_attributes()
def prepare_attributes(self):
"""Initialise anything that isn't already"""
if not self.windows:
self.windows = []
if not self.launcher_windows:
self.launcher_windows = []
if not self.terminals:
self.terminals = []
if not self.groups:
self.groups = []
if not self.config:
self.config = Config()
if self.groupsend == None:
self.groupsend = self.groupsend_type[self.config['broadcast_default']]
if not self.keybindings:
self.keybindings = Keybindings()
self.keybindings.configure(self.config['keybindings'])
if not self.style_providers:
self.style_providers = []
if not self.doing_layout:
self.doing_layout = False
if not self.pid_cwd:
self.pid_cwd = get_pid_cwd()
if self.gnome_client is None:
self.attempt_gnome_client()
self.connect_signals()
def connect_signals(self):
"""Connect all the gtk signals"""
self.gtk_settings=Gtk.Settings().get_default()
self.gtk_settings.connect('notify::gtk-theme-name', self.on_gtk_theme_name_notify)
self.cur_gtk_theme_name = self.gtk_settings.get_property('gtk-theme-name')
def set_origcwd(self, cwd):
"""Store the original cwd our process inherits"""
if cwd == '/':
cwd = os.path.expanduser('~')
os.chdir(cwd)
self.origcwd = cwd
def set_dbus_data(self, dbus_service):
"""Store the DBus bus details, if they are available"""
if dbus_service:
self.dbus_name = dbus_service.bus_name.get_name()
self.dbus_path = dbus_service.bus_path
def attempt_gnome_client(self):
"""Attempt to find a GNOME Session to register with"""
try:
from gi.repository import Gnome
self.gnome_program = Gnome.init(APP_NAME, APP_VERSION) # VERIFY FOR GTK3
self.gnome_client = Gnome.Ui.master_client() # VERIFY FOR GTK3
self.gnome_client.connect_to_session_manager()
self.gnome_client.connect('save-yourself', self.save_yourself)
self.gnome_client.connect('die', self.die)
dbg('GNOME session support enabled and registered')
except (ImportError, AttributeError):
self.gnome_client = False
dbg('GNOME session support not available')
def save_yourself(self, *args):
"""Save as much state as possible for the session manager"""
dbg('preparing session manager state')
# FIXME: Implement this
def die(self, *args):
"""Die at the hands of the session manager"""
dbg('session manager asked us to die')
# FIXME: Implement this
def get_windows(self):
"""Return a list of windows"""
return self.windows
def register_window(self, window):
"""Register a new window widget"""
if window not in self.windows:
dbg('Terminator::register_window: registering %s:%s' % (id(window),
type(window)))
self.windows.append(window)
def deregister_window(self, window):
"""de-register a window widget"""
dbg('Terminator::deregister_window: de-registering %s:%s' %
(id(window), type(window)))
if window in self.windows:
self.windows.remove(window)
else:
err('%s is not in registered window list' % window)
if len(self.windows) == 0:
# We have no windows left, we should exit
dbg('no windows remain, quitting')
Gtk.main_quit()
def register_launcher_window(self, window):
"""Register a new launcher window widget"""
if window not in self.launcher_windows:
dbg('Terminator::register_launcher_window: registering %s:%s' % (id(window),
type(window)))
self.launcher_windows.append(window)
def deregister_launcher_window(self, window):
"""de-register a launcher window widget"""
dbg('Terminator::deregister_launcher_window: de-registering %s:%s' %
(id(window), type(window)))
if window in self.launcher_windows:
self.launcher_windows.remove(window)
else:
err('%s is not in registered window list' % window)
if len(self.launcher_windows) == 0 and len(self.windows) == 0:
# We have no windows left, we should exit
dbg('no windows remain, quitting')
Gtk.main_quit()
def register_terminal(self, terminal):
"""Register a new terminal widget"""
if terminal not in self.terminals:
dbg('Terminator::register_terminal: registering %s:%s' %
(id(terminal), type(terminal)))
self.terminals.append(terminal)
def deregister_terminal(self, terminal):
"""De-register a terminal widget"""
dbg('Terminator::deregister_terminal: de-registering %s:%s' %
(id(terminal), type(terminal)))
self.terminals.remove(terminal)
if len(self.terminals) == 0:
dbg('no terminals remain, destroying all windows')
for window in self.windows:
window.destroy()
else:
dbg('Terminator::deregister_terminal: %d terminals remain' %
len(self.terminals))
def find_terminal_by_uuid(self, uuid):
"""Search our terminals for one matching the supplied UUID"""
dbg('searching self.terminals for: %s' % uuid)
for terminal in self.terminals:
dbg('checking: %s (%s)' % (terminal.uuid.urn, terminal))
if terminal.uuid.urn == uuid:
return terminal
return None
def find_window_by_uuid(self, uuid):
"""Search our terminals for one matching the supplied UUID"""
dbg('searching self.terminals for: %s' % uuid)
for window in self.windows:
dbg('checking: %s (%s)' % (window.uuid.urn, window))
if window.uuid.urn == uuid:
return window
return None
def new_window(self, cwd=None, profile=None):
"""Create a window with a Terminal in it"""
maker = Factory()
window = maker.make('Window')
terminal = maker.make('Terminal')
if cwd:
terminal.set_cwd(cwd)
if profile and self.config['always_split_with_profile']:
terminal.force_set_profile(None, profile)
window.add(terminal)
window.show(True)
terminal.spawn_child()
return(window, terminal)
def create_layout(self, layoutname):
"""Create all the parts necessary to satisfy the specified layout"""
layout = None
objects = {}
self.doing_layout = True
self.last_active_window = None
self.prelayout_windows = self.windows[:]
layout = copy.deepcopy(self.config.layout_get_config(layoutname))
if not layout:
# User specified a non-existent layout. default to one Terminal
err('layout %s not defined' % layout)
self.new_window()
return
# Wind the flat objects into a hierarchy
hierarchy = {}
count = 0
# Loop over the layout until we have consumed it, or hit 1000 loops.
# This is a stupid artificial limit, but it's safe.
while len(layout) > 0 and count < 1000:
count = count + 1
if count == 1000:
err('hit maximum loop boundary. THIS IS VERY LIKELY A BUG')
for obj in layout.keys():
if layout[obj]['type'].lower() == 'window':
hierarchy[obj] = {}
hierarchy[obj]['type'] = 'Window'
hierarchy[obj]['children'] = {}
# Copy any additional keys
for objkey in layout[obj].keys():
if layout[obj][objkey] != '' and not hierarchy[obj].has_key(objkey):
hierarchy[obj][objkey] = layout[obj][objkey]
objects[obj] = hierarchy[obj]
del(layout[obj])
else:
# Now examine children to see if their parents exist yet
if not layout[obj].has_key('parent'):
err('Invalid object: %s' % obj)
del(layout[obj])
continue
if objects.has_key(layout[obj]['parent']):
# Our parent has been created, add ourselves
childobj = {}
childobj['type'] = layout[obj]['type']
childobj['children'] = {}
# Copy over any additional object keys
for objkey in layout[obj].keys():
if not childobj.has_key(objkey):
childobj[objkey] = layout[obj][objkey]
objects[layout[obj]['parent']]['children'][obj] = childobj
objects[obj] = childobj
del(layout[obj])
layout = hierarchy
for windef in layout:
if layout[windef]['type'] != 'Window':
err('invalid layout format. %s' % layout)
raise(ValueError)
dbg('Creating a window')
window, terminal = self.new_window()
if layout[windef].has_key('position'):
parts = layout[windef]['position'].split(':')
if len(parts) == 2:
window.move(int(parts[0]), int(parts[1]))
if layout[windef].has_key('size'):
parts = layout[windef]['size']
winx = int(parts[0])
winy = int(parts[1])
if winx > 1 and winy > 1:
window.resize(winx, winy)
if layout[windef].has_key('title'):
window.title.force_title(layout[windef]['title'])
if layout[windef].has_key('maximised'):
if layout[windef]['maximised'] == 'True':
window.ismaximised = True
else:
window.ismaximised = False
window.set_maximised(window.ismaximised)
if layout[windef].has_key('fullscreen'):
if layout[windef]['fullscreen'] == 'True':
window.isfullscreen = True
else:
window.isfullscreen = False
window.set_fullscreen(window.isfullscreen)
window.create_layout(layout[windef])
self.layoutname = layoutname
def layout_done(self):
"""Layout operations have finished, record that fact"""
self.doing_layout = False
maker = Factory()
window_last_active_term_mapping = {}
for window in self.windows:
if window.is_child_notebook():
source = window.get_toplevel().get_children()[0]
else:
source = window
window_last_active_term_mapping[window] = copy.copy(source.last_active_term)
for terminal in self.terminals:
if not terminal.pid:
terminal.spawn_child()
for window in self.windows:
if window.is_child_notebook():
# For windows with a notebook
notebook = window.get_toplevel().get_children()[0]
# Cycle through pages by number
for page in xrange(0, notebook.get_n_pages()):
# Try and get the entry in the previously saved mapping
mapping = window_last_active_term_mapping[window]
page_last_active_term = mapping.get(notebook.get_nth_page(page), None)
if page_last_active_term is None:
# Couldn't find entry, so we find the first child of type Terminal
children = notebook.get_nth_page(page).get_children()
for page_last_active_term in children:
if maker.isinstance(page_last_active_term, 'Terminal'):
page_last_active_term = page_last_active_term.uuid
break
else:
err('Should never reach here!')
page_last_active_term = None
if page_last_active_term is None:
# Bail on this tab as we're having no luck here, continue with the next
continue
# Set the notebook entry, then ensure Terminal is visible and focussed
urn = page_last_active_term.urn
notebook.last_active_term[notebook.get_nth_page(page)] = page_last_active_term
if urn:
term = self.find_terminal_by_uuid(urn)
if term:
term.ensure_visible_and_focussed()
else:
# For windows without a notebook ensure Terminal is visible and focussed
if window_last_active_term_mapping[window]:
term = self.find_terminal_by_uuid(window_last_active_term_mapping[window].urn)
term.ensure_visible_and_focussed()
# Build list of new windows using prelayout list
new_win_list = []
for window in self.windows:
if window not in self.prelayout_windows:
new_win_list.append(window)
# Make sure all new windows get bumped to the top
for window in new_win_list:
window.show()
window.grab_focus()
try:
t = GdkX11.x11_get_server_time(window.get_window())
except (TypeError, AttributeError):
t = 0
window.get_window().focus(t)
# Awful workaround to be sure that the last focused window is actually the one focused.
# Don't ask, don't tell policy on this. Even this is not 100%
if self.last_active_window:
window = self.find_window_by_uuid(self.last_active_window.urn)
count = 0
while count < 1000 and Gtk.events_pending():
count += 1
Gtk.main_iteration_do(False)
window.show()
window.grab_focus()
try:
t = GdkX11.x11_get_server_time(window.get_window())
except (TypeError, AttributeError):
t = 0
window.get_window().focus(t)
self.prelayout_windows = None
def on_gtk_theme_name_notify(self, settings, prop):
"""Reconfigure if the gtk theme name changes"""
new_gtk_theme_name = settings.get_property(prop.name)
if new_gtk_theme_name != self.cur_gtk_theme_name:
self.cur_gtk_theme_name = new_gtk_theme_name
self.reconfigure()
def reconfigure(self):
"""Update configuration for the whole application"""
if self.style_providers != []:
for style_provider in self.style_providers:
Gtk.StyleContext.remove_provider_for_screen(
Gdk.Screen.get_default(),
style_provider)
self.style_providers = []
# Force the window background to be transparent for newer versions of
# GTK3. We then have to fix all the widget backgrounds because the
# widgets theming may not render it's own background.
css = """
.terminator-terminal-window {
background-color: alpha(@theme_bg_color,0); }
.terminator-terminal-window .notebook.header,
.terminator-terminal-window notebook header {
background-color: @theme_bg_color; }
.terminator-terminal-window .pane-separator {
background-color: @theme_bg_color; }
.terminator-terminal-window .terminator-terminal-searchbar {
background-color: @theme_bg_color; }
"""
# Fix several themes that put a borders, corners, or backgrounds around
# viewports, making the titlebar look bad.
css += """
.terminator-terminal-window GtkViewport,
.terminator-terminal-window viewport {
border-width: 0px;
border-radius: 0px;
background-color: transparent; }
"""
# Add per profile snippets for setting the background of the HBox
template = """
.terminator-profile-%s {
background-color: alpha(%s, %s); }
"""
profiles = self.config.base.profiles
for profile in profiles.keys():
if profiles[profile]['use_theme_colors']:
# Create a dummy window/vte and realise it so it has correct
# values to read from
tmp_win = Gtk.Window()
tmp_vte = Vte.Terminal()
tmp_win.add(tmp_vte)
tmp_win.realize()
bgcolor = tmp_vte.get_style_context().get_background_color(Gtk.StateType.NORMAL)
bgcolor = "#{0:02x}{1:02x}{2:02x}".format(int(bgcolor.red * 255),
int(bgcolor.green * 255),
int(bgcolor.blue * 255))
tmp_win.remove(tmp_vte)
del(tmp_vte)
del(tmp_win)
else:
bgcolor = Gdk.RGBA()
bgcolor = profiles[profile]['background_color']
if profiles[profile]['background_type'] == 'transparent':
bgalpha = profiles[profile]['background_darkness']
else:
bgalpha = "1"
munged_profile = "".join([c if c.isalnum() else "-" for c in profile])
css += template % (munged_profile, bgcolor, bgalpha)
style_provider = Gtk.CssProvider()
style_provider.load_from_data(css)
self.style_providers.append(style_provider)
# Attempt to load some theme specific stylistic tweaks for appearances
usr_theme_dir = os.path.expanduser('~/.local/share/themes')
(head, _tail) = os.path.split(borg.__file__)
app_theme_dir = os.path.join(head, 'themes')
theme_name = self.gtk_settings.get_property('gtk-theme-name')
theme_part_list = ['terminator.css']
if self.config['extra_styling']: # checkbox_style - needs adding to prefs
theme_part_list.append('terminator_styling.css')
for theme_part_file in theme_part_list:
for theme_dir in [usr_theme_dir, app_theme_dir]:
path_to_theme_specific_css = os.path.join(theme_dir,
theme_name,
'gtk-3.0/apps',
theme_part_file)
if os.path.isfile(path_to_theme_specific_css):
style_provider = Gtk.CssProvider()
style_provider.connect('parsing-error', self.on_css_parsing_error)
try:
style_provider.load_from_path(path_to_theme_specific_css)
except GError:
# Hmmm. Should we try to provide GTK version specific files here on failure?
gtk_version_string = '.'.join([str(Gtk.get_major_version()),
str(Gtk.get_minor_version()),
str(Gtk.get_micro_version())])
err('Error(s) loading css from %s into Gtk %s' % (path_to_theme_specific_css,
gtk_version_string))
self.style_providers.append(style_provider)
break
# Size the GtkPaned splitter handle size.
css = ""
if self.config['handle_size'] in xrange(0, 21):
css += """
.terminator-terminal-window GtkPaned,
.terminator-terminal-window paned {
-GtkPaned-handle-size: %s; }
""" % self.config['handle_size']
style_provider = Gtk.CssProvider()
style_provider.load_from_data(css)
self.style_providers.append(style_provider)
# Apply the providers, incrementing priority so they don't cancel out
# each other
for idx in xrange(0, len(self.style_providers)):
Gtk.StyleContext.add_provider_for_screen(
Gdk.Screen.get_default(),
self.style_providers[idx],
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION+idx)
# Cause all the terminals to reconfigure
for terminal in self.terminals:
terminal.reconfigure()
# Reparse our keybindings
self.keybindings.configure(self.config['keybindings'])
# Update tab position if appropriate
maker = Factory()
for window in self.windows:
child = window.get_child()
if maker.isinstance(child, 'Notebook'):
child.configure()
def on_css_parsing_error(self, provider, section, error, user_data=None):
"""Report CSS parsing issues"""
file_path = section.get_file().get_path()
line_no = section.get_end_line() +1
col_no = section.get_end_position() + 1
err('%s, at line %d, column %d, of file %s' % (error.message,
line_no, col_no,
file_path))
def create_group(self, name):
"""Create a new group"""
if name not in self.groups:
dbg('Terminator::create_group: registering group %s' % name)
self.groups.append(name)
def closegroupedterms(self, group):
"""Close all terminals in a group"""
for terminal in self.terminals[:]:
if terminal.group == group:
terminal.close()
def group_hoover(self):
"""Clean out unused groups"""
if self.config['autoclean_groups']:
inuse = []
todestroy = []
for terminal in self.terminals:
if terminal.group:
if not terminal.group in inuse:
inuse.append(terminal.group)
for group in self.groups:
if not group in inuse:
todestroy.append(group)
dbg('Terminator::group_hoover: %d groups, hoovering %d' %
(len(self.groups), len(todestroy)))
for group in todestroy:
self.groups.remove(group)
def group_emit(self, terminal, group, type, event):
"""Emit to each terminal in a group"""
dbg('Terminator::group_emit: emitting a keystroke for group %s' %
group)
for term in self.terminals:
if term != terminal and term.group == group:
term.vte.emit(type, eventkey2gdkevent(event))
def all_emit(self, terminal, type, event):
"""Emit to all terminals"""
for term in self.terminals:
if term != terminal:
term.vte.emit(type, eventkey2gdkevent(event))
def do_enumerate(self, widget, pad):
"""Insert the number of each terminal in a group, into that terminal"""
if pad:
numstr = '%0'+str(len(str(len(self.terminals))))+'d'
else:
numstr = '%d'
terminals = []
for window in self.windows:
containers, win_terminals = enumerate_descendants(window)
terminals.extend(win_terminals)
for term in self.get_target_terms(widget):
idx = terminals.index(term)
term.feed(numstr % (idx + 1))
def get_sibling_terms(self, widget):
termset = []
for term in self.terminals:
if term.group == widget.group:
termset.append(term)
return(termset)
def get_target_terms(self, widget):
"""Get the terminals we should currently be broadcasting to"""
if self.groupsend == self.groupsend_type['all']:
return(self.terminals)
elif self.groupsend == self.groupsend_type['group']:
if widget.group != None:
return(self.get_sibling_terms(widget))
return([widget])
def get_focussed_terminal(self):
"""iterate over all the terminals to find which, if any, has focus"""
for terminal in self.terminals:
if terminal.has_focus():
return(terminal)
return(None)
def focus_changed(self, widget):
"""We just moved focus to a new terminal"""
for terminal in self.terminals:
terminal.titlebar.update(widget)
return
def focus_left(self, widget):
self.last_focused_term=widget
def describe_layout(self):
"""Describe our current layout"""
layout = {}
count = 0
for window in self.windows:
parent = ''
count = window.describe_layout(count, parent, layout, 0)
return(layout)
# vim: set expandtab ts=4 sw=4:
terminator-1.91/terminatorlib/version.py 0000664 0001750 0001750 00000001622 13054612071 021042 0 ustar steve steve 0000000 0000000 #!/usr/bin/env python2
# TerminatorVersion - version number
# 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
# 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
"""TerminatorVersion by Chris Jones
TerminatorVersion supplies our version number.
"""
APP_NAME = 'terminator'
APP_VERSION = '1.91'
terminator-1.91/terminatorlib/titlebar.py 0000775 0001750 0001750 00000030322 13054612071 021165 0 ustar steve steve 0000000 0000000 #!/usr/bin/env python2
# Terminator by Chris Jones
# GPL v2 only
"""titlebar.py - classes necessary to provide a terminal title bar"""
from gi.repository import Gtk, Gdk
from gi.repository import GObject
from gi.repository import Pango
import random
import itertools
from version import APP_NAME
from util import dbg
from terminator import Terminator
from editablelabel import EditableLabel
from translation import _
# 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
bellicon = None
__gsignals__ = {
'clicked': (GObject.SignalFlags.RUN_LAST, None, ()),
'edit-done': (GObject.SignalFlags.RUN_LAST, None, ()),
'create-group': (GObject.SignalFlags.RUN_LAST, None,
(GObject.TYPE_STRING,)),
}
def __init__(self, terminal):
"""Class initialiser"""
GObject.GObject.__init__(self)
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(ellipsize='end')
self.groupicon = Gtk.Image()
self.bellicon = Gtk.Image()
self.bellicon.set_no_show_all(True)
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.IconSize.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()
self.bellicon.set_from_icon_name('terminal-bell', Gtk.IconSize.MENU)
viewport = Gtk.Viewport(hscroll_policy='natural')
viewport.add(self.label)
hbox = Gtk.HBox()
hbox.pack_start(self.ebox, False, True, 0)
hbox.pack_start(Gtk.VSeparator(), False, True, 0)
hbox.pack_start(viewport, True, True, 0)
hbox.pack_end(self.bellicon, False, False, 2)
self.add(hbox)
hbox.show_all()
self.set_no_show_all(True)
self.show()
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-press-event', func)
def update(self, other=None):
"""Update our contents"""
default_bg = False
if self.config['title_hide_sizetext']:
self.label.set_text("%s" % self.termtext)
else:
self.label.set_text("%s %s" % (self.termtext, self.sizetext))
if (not self.config['title_use_system_font']) and self.config['title_font']:
title_font = Pango.FontDescription(self.config['title_font'])
else:
title_font = Pango.FontDescription(self.config.get_system_prop_font())
self.label.modify_font(title_font)
self.grouplabel.modify_font(title_font)
if other:
term = self.terminal
terminator = self.terminator
if other == 'window-focus-out':
title_fg = self.config['title_inactive_fg_color']
title_bg = self.config['title_inactive_bg_color']
icon = '_receive_off'
default_bg = True
group_fg = self.config['title_inactive_fg_color']
group_bg = self.config['title_inactive_bg_color']
elif 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'
default_bg = True
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'
default_bg = True
group_fg = self.config['title_inactive_fg_color']
group_bg = self.config['title_inactive_bg_color']
else:
# We're the active terminal
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.StateType.NORMAL,
Gdk.color_parse(title_fg))
self.grouplabel.modify_fg(Gtk.StateType.NORMAL,
Gdk.color_parse(group_fg))
self.modify_bg(Gtk.StateType.NORMAL,
Gdk.color_parse(title_bg))
if not self.get_desired_visibility():
if default_bg == True:
color = term.get_style_context().get_background_color(Gtk.StateType.NORMAL) # VERIFY FOR GTK3
else:
color = Gdk.color_parse(title_bg)
self.update_visibility()
self.ebox.modify_bg(Gtk.StateType.NORMAL,
Gdk.color_parse(group_bg))
self.set_from_icon_name(icon, Gtk.IconSize.MENU)
def update_visibility(self):
"""Make the titlebar be visible or not"""
if not self.get_desired_visibility():
dbg('hiding titlebar')
self.hide()
self.label.hide()
else:
dbg('showing titlebar')
self.show()
self.label.show()
def get_desired_visibility(self):
"""Returns True if the titlebar is supposed to be visible. False if
not"""
if self.editing() == True or self.terminal.group:
dbg('implicit desired visibility')
return(True)
else:
dbg('configured visibility: %s' % self.config['show_titlebar'])
return(self.config['show_titlebar'])
def set_from_icon_name(self, name, size = Gtk.IconSize.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.set_text('')
self.grouplabel.hide()
self.update_visibility()
def on_clicked(self, widget, event):
"""Handle a click on the label"""
self.show()
self.label.show()
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"""
if self.terminal.group:
self.groupentry.set_text(self.terminal.group)
else:
defaultmembers=[_('Alpha'),_('Beta'),_('Gamma'),_('Delta'),_('Epsilon'),_('Zeta'),_('Eta'),
_('Theta'),_('Iota'),_('Kappa'),_('Lambda'),_('Mu'),_('Nu'),_('Xi'),
_('Omicron'),_('Pi'),_('Rho'),_('Sigma'),_('Tau'),_('Upsilon'),_('Phi'),
_('Chi'),_('Psi'),_('Omega')]
currentgroups=set(self.terminator.groups)
for i in range(1,4):
defaultgroups=set(map(''.join, list(itertools.product(defaultmembers,repeat=i))))
freegroups = list(defaultgroups-currentgroups)
if freegroups:
self.groupentry.set_text(random.choice(freegroups))
break
else:
self.groupentry.set_text('')
self.groupentry.show()
self.grouplabel.hide()
self.groupentry.grab_focus()
self.update_visibility()
def groupentry_cancel(self, widget, event):
"""Hide the group name entry"""
self.groupentry.set_text('')
self.groupentry.hide()
self.grouplabel.show()
self.get_parent().grab_focus()
def groupentry_activate(self, widget):
"""Actually cause a group to be created"""
groupname = self.groupentry.get_text() or None
dbg('Titlebar::groupentry_activate: creating group: %s' % groupname)
self.groupentry_cancel(None, None)
last_focused_term=self.terminator.last_focused_term
if self.terminal.targets_for_new_group:
[term.titlebar.emit('create-group', groupname) for term in self.terminal.targets_for_new_group]
self.terminal.targets_for_new_group = None
else:
self.emit('create-group', groupname)
last_focused_term.grab_focus()
self.terminator.focus_changed(last_focused_term)
def groupentry_keypress(self, widget, event):
"""Handle keypresses on the entry widget"""
key = Gdk.keyval_name(event.keyval)
if key == 'Escape':
self.groupentry_cancel(None, None)
def icon_bell(self):
"""A bell signal requires we display our bell icon"""
self.bellicon.show()
GObject.timeout_add(1000, self.icon_bell_hide)
def icon_bell_hide(self):
"""Handle a timeout which means we now hide the bell icon"""
self.bellicon.hide()
return(False)
def get_custom_string(self):
"""If we have a custom string set, return it, otherwise None"""
if self.label.is_custom():
return(self.label.get_text())
else:
return(None)
def set_custom_string(self, string):
"""Set a custom string"""
self.label.set_text(string)
self.label.set_custom()
GObject.type_register(Titlebar)
terminator-1.91/terminatorlib/paned.py 0000775 0001750 0001750 00000050347 13054612071 020457 0 ustar steve steve 0000000 0000000 #!/usr/bin/env python2
# Terminator by Chris Jones
# GPL v2 only
"""paned.py - a base Paned container class and the vertical/horizontal
variants"""
import time
from gi.repository import GObject, Gtk, Gdk
from util import dbg, err, enumerate_descendants
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"""
position = None
maker = None
ratio = 0.5
last_balance_time = 0
last_balance_args = None
def __init__(self):
"""Class initialiser"""
self.terminator = Terminator()
self.maker = Factory()
Container.__init__(self)
self.signals.append({'name': 'resize-term',
'flags': GObject.SignalFlags.RUN_LAST,
'return_type': None,
'param_types': (GObject.TYPE_STRING,)})
# pylint: disable-msg=W0613
def split_axis(self, widget, vertical=True, cwd=None, sibling=None,
widgetfirst=True):
"""Default axis splitter. This should be implemented by subclasses"""
order = None
self.remove(widget)
if vertical:
container = VPaned()
else:
container = HPaned()
self.get_toplevel().set_pos_by_ratio = True
if not sibling:
sibling = self.maker.make('terminal')
sibling.set_cwd(cwd)
if self.config['always_split_with_profile']:
sibling.force_set_profile(None, widget.get_profile())
sibling.spawn_child()
if widget.group and self.config['split_to_group']:
sibling.set_group(None, widget.group)
elif self.config['always_split_with_profile']:
sibling.force_set_profile(None, widget.get_profile())
self.add(container)
self.show_all()
order = [widget, sibling]
if widgetfirst is False:
order.reverse()
for terminal in order:
container.add(terminal)
self.show_all()
sibling.grab_focus()
while Gtk.events_pending():
Gtk.main_iteration_do(False)
self.get_toplevel().set_pos_by_ratio = False
def add(self, widget, metadata=None):
"""Add a widget to the container"""
if len(self.children) == 0:
self.pack1(widget, False, True)
self.children.append(widget)
elif len(self.children) == 1:
if self.get_child1():
self.pack2(widget, False, True)
else:
self.pack1(widget, False, True)
self.children.append(widget)
else:
raise ValueError('Paned widgets can only have two children')
if self.maker.isinstance(widget, 'Terminal'):
top_window = self.get_toplevel()
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,
'size-allocate': self.new_size,
'zoom': top_window.zoom,
'tab-change': top_window.tab_change,
'group-all': top_window.group_all,
'group-all-toggle': top_window.group_all_toggle,
'ungroup-all': top_window.ungroup_all,
'group-tab': top_window.group_tab,
'group-tab-toggle': top_window.group_tab_toggle,
'ungroup-tab': top_window.ungroup_tab,
'move-tab': top_window.move_tab,
'maximise': [top_window.zoom, False],
'tab-new': [top_window.tab_new, widget],
'navigate': top_window.navigate_terminal,
'rotate-cw': [top_window.rotate, True],
'rotate-ccw': [top_window.rotate, False]}
for signal in signals:
args = []
handler = signals[signal]
if isinstance(handler, list):
args = handler[1:]
handler = handler[0]
self.connect_child(widget, signal, handler, *args)
if metadata and \
metadata.has_key('had_focus') and \
metadata['had_focus'] == True:
widget.grab_focus()
elif isinstance(widget, Gtk.Paned):
try:
self.connect_child(widget, 'resize-term', self.resizeterm)
self.connect_child(widget, 'size-allocate', self.new_size)
except TypeError:
err('Paned::add: %s has no signal resize-term' % widget)
def on_button_press(self, widget, event):
"""Handle button presses on a Pane"""
if event.button == 1 and event.type == Gdk.EventType._2BUTTON_PRESS:
if event.get_state() & Gdk.ModifierType.MOD4_MASK == Gdk.ModifierType.MOD4_MASK:
recurse_up=True
else:
recurse_up=False
if event.get_state() & Gdk.ModifierType.SHIFT_MASK == Gdk.ModifierType.SHIFT_MASK:
recurse_down=True
else:
recurse_down=False
self.last_balance_time = time.time()
self.last_balance_args = (recurse_up, recurse_down)
return True
else:
return False
def on_button_release(self, widget, event):
"""Handle button presses on a Pane"""
if event.button == 1:
if self.last_balance_time > (time.time() - 1):
# Dumb loop still needed, or some terms get squished on a Super rebalance
for i in range(3):
while Gtk.events_pending():
Gtk.main_iteration_do(False)
self.do_redistribute(*self.last_balance_args)
return False
def set_autoresize(self, autoresize):
"""Must be called on the highest ancestor in one given orientation"""
"""TODO write some better doc :)"""
maker = Factory()
children = self.get_children()
self.child_set_property(children[0], 'resize', False)
self.child_set_property(children[1], 'resize', not autoresize)
for child in children:
if maker.type(child) == maker.type(self):
child.set_autoresize(autoresize)
def do_redistribute(self, recurse_up=False, recurse_down=False):
"""Evenly divide available space between sibling panes"""
maker = Factory()
#1 Find highest ancestor of the same type => ha
highest_ancestor = self
while type(highest_ancestor.get_parent()) == type(highest_ancestor):
highest_ancestor = highest_ancestor.get_parent()
highest_ancestor.set_autoresize(False)
# (1b) If Super modifier, redistribute higher sections too
if recurse_up:
grandfather=highest_ancestor.get_parent()
if maker.isinstance(grandfather, 'VPaned') or \
maker.isinstance(grandfather, 'HPaned') :
grandfather.do_redistribute(recurse_up, recurse_down)
highest_ancestor._do_redistribute(recurse_up, recurse_down)
GObject.idle_add(highest_ancestor.set_autoresize, True)
def _do_redistribute(self, recurse_up=False, recurse_down=False):
maker = Factory()
#2 Make a list of self + all children of same type
tree = [self, [], 0, None]
toproc = [tree]
number_splits = 1
while toproc:
curr = toproc.pop(0)
for child in curr[0].get_children():
if type(child) == type(curr[0]):
childset = [child, [], 0, curr]
curr[1].append(childset)
toproc.append(childset)
number_splits = number_splits+1
else:
curr[1].append([None,[], 1, None])
p = curr
while p:
p[2] = p[2] + 1
p = p[3]
# (1c) If Shift modifier, redistribute lower sections too
if recurse_down and \
(maker.isinstance(child, 'VPaned') or \
maker.isinstance(child, 'HPaned')):
child.do_redistribute(False, True)
#3 Get ancestor x/y => a, and handle size => hs
avail_pixels=self.get_length()
handle_size = self.get_handlesize()
#4 Math! eek (a - (n * hs)) / (n + 1) = single size => s
single_size = (avail_pixels - (number_splits * handle_size)) / (number_splits + 1)
arr_sizes = [single_size]*(number_splits+1)
for i in range(avail_pixels % (number_splits + 1)):
arr_sizes[i] = arr_sizes[i] + 1
#5 Descend down setting the handle position to s
# (Has to handle nesting properly)
toproc = [tree]
while toproc:
curr = toproc.pop(0)
for child in curr[1]:
toproc.append(child)
if curr[1].index(child) == 0:
curr[0].set_position((child[2]*single_size)+((child[2]-1)*handle_size))
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 get_children(self):
"""Return an ordered list of our children"""
children = []
children.append(self.get_child1())
children.append(self.get_child2())
return(children)
def get_child_metadata(self, widget):
"""Return metadata about a child"""
metadata = {}
metadata['had_focus'] = widget.has_focus()
def get_handlesize(self):
"""Why oh why, gtk3?"""
try:
value = GObject.Value(int)
self.style_get_property('handle-size', value)
return(value.get_int())
except:
return 0
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]
first_term_sibling = sibling
cur_tabnum = None
focus_sibling = True
if self.get_toplevel().is_child_notebook():
notebook = self.get_toplevel().get_children()[0]
cur_tabnum = notebook.get_current_page()
tabnum = notebook.page_num_descendant(self)
nth_page = notebook.get_nth_page(tabnum)
exiting_term_was_last_active = (notebook.last_active_term[nth_page] == widget.uuid)
if exiting_term_was_last_active:
first_term_sibling = enumerate_descendants(self)[1][0]
notebook.set_last_active_term(first_term_sibling.uuid)
notebook.clean_last_active_term()
self.get_toplevel().last_active_term = None
if cur_tabnum != tabnum:
focus_sibling = False
elif self.get_toplevel().last_active_term != widget.uuid:
focus_sibling = False
self.remove(sibling)
metadata = None
parent = self.get_parent()
metadata = parent.get_child_metadata(self)
dbg('metadata obtained for %s: %s' % (self, metadata))
parent.remove(self)
self.cnxids.remove_all()
parent.add(sibling, metadata)
if cur_tabnum:
notebook.set_current_page(cur_tabnum)
if focus_sibling:
first_term_sibling.grab_focus()
elif not sibling.get_toplevel().is_child_notebook():
Terminator().find_terminal_by_uuid(sibling.get_toplevel().last_active_term.urn).grab_focus()
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()
child = self.children[0]
self.remove(child)
parent.replace(self, child)
del(self)
def resizeterm(self, widget, keyname):
"""Handle a keyboard event requesting a terminal resize"""
if keyname in ['up', 'down'] and isinstance(self, Gtk.VPaned):
# This is a key we can handle
position = self.get_position()
if self.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 self.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)
def create_layout(self, layout):
"""Apply layout configuration"""
if not layout.has_key('children'):
err('layout specifies no children: %s' % layout)
return
children = layout['children']
if len(children) != 2:
# Paned widgets can only have two children
err('incorrect number of children for Paned: %s' % layout)
return
keys = []
# FIXME: This seems kinda ugly. All we want here is to know the order
# of children based on child['order']
try:
child_order_map = {}
for child in children:
key = children[child]['order']
child_order_map[key] = child
map_keys = child_order_map.keys()
map_keys.sort()
for map_key in map_keys:
keys.append(child_order_map[map_key])
except KeyError:
# We've failed to figure out the order. At least give the terminals
# in the wrong order
keys = children.keys()
num = 0
for child_key in keys:
child = children[child_key]
dbg('Making a child of type: %s' % child['type'])
if child['type'] == 'Terminal':
pass
elif child['type'] == 'VPaned':
if num == 0:
terminal = self.get_child1()
else:
terminal = self.get_child2()
self.split_axis(terminal, True)
elif child['type'] == 'HPaned':
if num == 0:
terminal = self.get_child1()
else:
terminal = self.get_child2()
self.split_axis(terminal, False)
else:
err('unknown child type: %s' % child['type'])
num = num + 1
self.get_child1().create_layout(children[keys[0]])
self.get_child2().create_layout(children[keys[1]])
# Set the position with ratio. For some reason more reliable than by pos.
if layout.has_key('ratio'):
self.ratio = float(layout['ratio'])
self.set_position_by_ratio()
def grab_focus(self):
"""We don't want focus, we want a Terminal to have it"""
self.get_child1().grab_focus()
def rotate_recursive(self, parent, w, h, clockwise):
"""
Recursively rotate "self" into a new paned that'll have "w" x "h" size. Attach it to "parent".
As discussed in LP#1522542, we should build up the new layout (including the separator positions)
in a single step. We can't rely on Gtk+ computing the allocation sizes yet, so we have to do the
computation ourselves and carry the resulting paned sizes all the way down the widget tree.
"""
maker = Factory()
handle_size = self.get_handlesize()
if isinstance(self, HPaned):
container = VPaned()
reverse = not clockwise
else:
container = HPaned()
reverse = clockwise
container.ratio = self.ratio
children = self.get_children()
if reverse:
container.ratio = 1 - container.ratio
children.reverse()
if isinstance(self, HPaned):
w1 = w2 = w
h1 = pos = self.position_by_ratio(h, handle_size, container.ratio)
h2 = max(h - h1 - handle_size, 0)
else:
h1 = h2 = h
w1 = pos = self.position_by_ratio(w, handle_size, container.ratio)
w2 = max(w - w1 - handle_size, 0)
container.set_pos(pos)
parent.add(container)
if maker.isinstance(children[0], 'Terminal'):
children[0].get_parent().remove(children[0])
container.add(children[0])
else:
children[0].rotate_recursive(container, w1, h1, clockwise)
if maker.isinstance(children[1], 'Terminal'):
children[1].get_parent().remove(children[1])
container.add(children[1])
else:
children[1].rotate_recursive(container, w2, h2, clockwise)
def new_size(self, widget, allocation):
if self.get_toplevel().set_pos_by_ratio:
self.set_position_by_ratio()
else:
self.set_position(self.get_position())
def position_by_ratio(self, total_size, handle_size, ratio):
non_separator_size = max(total_size - handle_size, 0)
ratio = min(max(ratio, 0.0), 1.0)
return int(round(non_separator_size * ratio))
def ratio_by_position(self, total_size, handle_size, position):
non_separator_size = max(total_size - handle_size, 0)
if non_separator_size == 0:
return None
position = min(max(position, 0), non_separator_size)
return float(position) / float(non_separator_size)
def set_position_by_ratio(self):
# Fix for strange race condition where every so often get_length returns 1. (LP:1655027)
while self.terminator.doing_layout and self.get_length() == 1:
while Gtk.events_pending():
Gtk.main_iteration()
self.set_pos(self.position_by_ratio(self.get_length(), self.get_handlesize(), self.ratio))
def set_position(self, pos):
newratio = self.ratio_by_position(self.get_length(), self.get_handlesize(), pos)
if newratio is not None:
self.ratio = newratio
self.set_pos(pos)
class HPaned(Paned, Gtk.HPaned):
"""Merge Gtk.HPaned into our base Paned Container"""
def __init__(self):
"""Class initialiser"""
Paned.__init__(self)
GObject.GObject.__init__(self)
self.register_signals(HPaned)
self.cnxids.new(self, 'button-press-event', self.on_button_press)
self.cnxids.new(self, 'button-release-event', self.on_button_release)
def get_length(self):
return(self.get_allocated_width())
def set_pos(self, pos):
Gtk.HPaned.set_position(self, pos)
self.set_property('position-set', True)
class VPaned(Paned, Gtk.VPaned):
"""Merge Gtk.VPaned into our base Paned Container"""
def __init__(self):
"""Class initialiser"""
Paned.__init__(self)
GObject.GObject.__init__(self)
self.register_signals(VPaned)
self.cnxids.new(self, 'button-press-event', self.on_button_press)
self.cnxids.new(self, 'button-release-event', self.on_button_release)
def get_length(self):
return(self.get_allocated_height())
def set_pos(self, pos):
Gtk.VPaned.set_position(self, pos)
self.set_property('position-set', True)
GObject.type_register(HPaned)
GObject.type_register(VPaned)
# vim: set expandtab ts=4 sw=4:
terminator-1.91/terminatorlib/plugin.py 0000775 0001750 0001750 00000014455 13054612071 020666 0 ustar steve steve 0000000 0000000 #!/usr/bin/env python2
# Terminator by Chris Jones
# GPL v2 only
"""plugin.py - Base plugin system
Inspired by Armin Ronacher's post at
http://lucumr.pocoo.org/2006/7/3/python-plugin-system
Used with permission (the code in that post is to be
considered BSD licenced, per the authors wishes)
>>> registry = PluginRegistry()
>>> registry.instances
{}
>>> registry.load_plugins(True)
>>> plugins = registry.get_plugins_by_capability('test')
>>> len(plugins)
1
>>> plugins[0] #doctest: +ELLIPSIS
>>> registry.get_plugins_by_capability('this_should_not_ever_exist')
[]
>>> plugins[0].do_test()
'TestPluginWin'
"""
import sys
import os
import borg
from config import Config
from util import dbg, err, get_config_dir
from terminator import Terminator
class Plugin(object):
"""Definition of our base plugin class"""
capabilities = None
def __init__(self):
"""Class initialiser."""
pass
def unload(self):
"""Prepare to be unloaded"""
pass
class PluginRegistry(borg.Borg):
"""Definition of a class to store plugin instances"""
available_plugins = None
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
if not self.available_plugins:
self.available_plugins = {}
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:
if plugin == '__init__.py':
continue
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 item not in self.available_plugins.keys():
func = getattr(module, item)
self.available_plugins[item] = func
if not testing and item not in config['enabled_plugins']:
dbg('plugin %s not enabled, skipping' % item)
continue
if item not in self.instances:
self.instances[item] = func()
except Exception, ex:
err('PluginRegistry::load_plugins: Importing plugin %s \
failed: %s' % (plugin, ex))
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)
def get_available_plugins(self):
"""Return a list of all available plugins whether they are enabled or
disabled"""
return(self.available_plugins.keys())
def is_enabled(self, plugin):
"""Return a boolean value indicating whether a plugin is enabled or
not"""
return(self.instances.has_key(plugin))
def enable(self, plugin):
"""Enable a plugin"""
if plugin in self.instances:
err("Cannot enable plugin %s, already enabled" % plugin)
dbg("Enabling %s" % plugin)
self.instances[plugin] = self.available_plugins[plugin]()
def disable(self, plugin):
"""Disable a plugin"""
dbg("Disabling %s" % plugin)
self.instances[plugin].unload()
del(self.instances[plugin])
# 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
nameopen = None
namecopy = None
def __init__(self):
"""Class initialiser"""
Plugin.__init__(self)
terminator = Terminator()
for terminal in terminator.terminals:
terminal.match_add(self.handler_name, self.match)
def callback(self, url):
"""Callback to transform the enclosed URL"""
raise NotImplementedError
def unload(self):
"""Handle being removed"""
if not self.match:
err('unload called without self.handler_name being set')
return
terminator = Terminator()
for terminal in terminator.terminals:
terminal.match_remove(self.handler_name)
# 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
terminator-1.91/terminatorlib/optionparse.py 0000775 0001750 0001750 00000015351 13054612071 021727 0 ustar steve steve 0000000 0000000 #!/usr/bin/env python2
# 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
from translation import _
options = None
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]"
is_x_terminal_emulator = os.path.basename(sys.argv[0]) == 'x-terminal-emulator'
parser = OptionParser(usage)
parser.add_option('-v', '--version', action='store_true', dest='version',
help=_('Display program version'))
parser.add_option('-m', '--maximise', action='store_true', dest='maximise',
help=_('Maximize 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)'))
if not is_x_terminal_emulator:
parser.add_option('-e', '--command', dest='command',
help=_('Specify a command to execute inside the terminal'))
else:
parser.add_option('--command', dest='command',
help=_('Specify a command to execute inside the terminal'))
parser.add_option('-e', '--execute2', 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('-g', '--config', dest='config',
help=_('Specify a config file'))
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('-c', '--classname', dest='classname', help=_('Set a \
custom name (WM_CLASS) property on the window'))
parser.add_option('-i', '--icon', dest='forcedicon', help=_('Set a custom \
icon for the window (by file or name)'))
parser.add_option('-r', '--role', dest='role',
help=_('Set a custom WM_WINDOW_ROLE property on the window'))
parser.add_option('-l', '--layout', dest='layout',
help=_('Launch with the given layout'))
parser.add_option('-s', '--select-layout', action='store_true',
dest='select', help=_('Select a layout from a list'))
parser.add_option('-p', '--profile', dest='profile',
help=_('Use a different profile as the default'))
parser.add_option('-u', '--no-dbus', action='store_true', dest='nodbus',
help=_('Disable DBus'))
parser.add_option('-d', '--debug', action='count', dest='debug',
help=_('Enable debugging information (twice for debug server)'))
parser.add_option('--debug-classes', action='store', dest='debug_classes',
help=_('Comma separated list of classes to limit debugging to'))
parser.add_option('--debug-methods', action='store', dest='debug_methods',
help=_('Comma separated list of methods to limit debugging to'))
parser.add_option('--new-tab', action='store_true', dest='new_tab',
help=_('If Terminator is already running, just open a new tab'))
for item in ['--sm-client-id', '--sm-config-prefix', '--screen', '-n',
'--no-gconf' ]:
parser.add_option(item, dest='dummy', action='store',
help=SUPPRESS_HELP)
global options
(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_classes or options.debug_methods:
if not options.debug > 0:
options.debug = 1
if options.debug:
util.DEBUG = True
if options.debug > 1:
util.DEBUGFILES = True
if options.debug_classes:
classes = options.debug_classes.split(',')
for item in classes:
util.DEBUGCLASSES.append(item.strip())
if options.debug_methods:
methods = options.debug_methods.split(',')
for item in methods:
util.DEBUGMETHODS.append(item.strip())
if options.working_directory:
if os.path.exists(os.path.expanduser(options.working_directory)):
options.working_directory = os.path.expanduser(options.working_directory)
os.chdir(options.working_directory)
else:
err('OptionParse::parse_options: %s does not exist' %
options.working_directory)
options.working_directory = ''
if options.layout is None:
options.layout = 'default'
configobj = config.Config()
if options.profile and options.profile not in configobj.list_profiles():
options.profile = None
configobj.options_set(options)
if util.DEBUG == True:
dbg('OptionParse::parse_options: command line options: %s' % options)
return(options)
terminator-1.91/terminatorlib/config.py 0000775 0001750 0001750 00000075177 13054612071 020645 0 ustar steve steve 0000000 0000000 #!/usr/bin/env python2
# TerminatorConfig - layered config classes
# 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 by Chris Jones
Classes relating to configuration
>>> DEFAULTS['global_config']['focus']
'click'
>>> config = Config()
>>> config['focus'] = 'sloppy'
>>> config['focus']
'sloppy'
>>> DEFAULTS['global_config']['focus']
'click'
>>> config2 = Config()
>>> config2['focus']
'sloppy'
>>> config2['focus'] = 'click'
>>> config2['focus']
'click'
>>> config['focus']
'click'
>>> config['geometry_hinting'].__class__.__name__
'bool'
>>> plugintest = {}
>>> plugintest['foo'] = 'bar'
>>> config.plugin_set_config('testplugin', plugintest)
>>> config.plugin_get_config('testplugin')
{'foo': 'bar'}
>>> config.plugin_get('testplugin', 'foo')
'bar'
>>> config.plugin_get('testplugin', 'foo', 'new')
'bar'
>>> config.plugin_get('testplugin', 'algo')
Traceback (most recent call last):
...
KeyError: 'ConfigBase::get_item: unknown key algo'
>>> config.plugin_get('testplugin', 'algo', 1)
1
>>> config.plugin_get('anothertestplugin', 'algo', 500)
500
>>> config.get_profile()
'default'
>>> config.set_profile('my_first_new_testing_profile')
>>> config.get_profile()
'my_first_new_testing_profile'
>>> config.del_profile('my_first_new_testing_profile')
>>> config.get_profile()
'default'
>>> config.list_profiles().__class__.__name__
'list'
>>> config.options_set({})
>>> config.options_get()
{}
>>>
"""
import platform
import os
from copy import copy
from configobj.configobj import ConfigObj, flatten_errors
from configobj.validate import Validator
from borg import Borg
from util import dbg, err, DEBUG, get_config_dir, dict_diff
from gi.repository import Gio
DEFAULTS = {
'global_config': {
'dbus' : True,
'focus' : 'click',
'handle_size' : -1,
'geometry_hinting' : False,
'window_state' : 'normal',
'borderless' : False,
'extra_styling' : True,
'tab_position' : 'top',
'broadcast_default' : 'group',
'close_button_on_tab' : True,
'hide_tabbar' : False,
'scroll_tabbar' : False,
'homogeneous_tabbar' : True,
'hide_from_taskbar' : False,
'always_on_top' : False,
'hide_on_lose_focus' : False,
'sticky' : False,
'use_custom_url_handler': False,
'custom_url_handler' : '',
'disable_real_transparency' : False,
'title_hide_sizetext' : False,
'title_transmit_fg_color' : '#ffffff',
'title_transmit_bg_color' : '#c80003',
'title_receive_fg_color' : '#ffffff',
'title_receive_bg_color' : '#0076c9',
'title_inactive_fg_color' : '#000000',
'title_inactive_bg_color' : '#c0bebf',
'inactive_color_offset': 0.8,
'enabled_plugins' : ['LaunchpadBugURLHandler',
'LaunchpadCodeURLHandler',
'APTURLHandler'],
'suppress_multiple_term_dialog': False,
'always_split_with_profile': False,
'title_use_system_font' : True,
'title_font' : 'Sans 9',
'putty_paste_style' : False,
'smart_copy' : True,
},
'keybindings': {
'zoom_in' : 'plus',
'zoom_out' : 'minus',
'zoom_normal' : '0',
'new_tab' : 't',
'cycle_next' : 'Tab',
'cycle_prev' : 'Tab',
'go_next' : 'n',
'go_prev' : 'p',
'go_up' : 'Up',
'go_down' : 'Down',
'go_left' : 'Left',
'go_right' : 'Right',
'rotate_cw' : 'r',
'rotate_ccw' : 'r',
'split_horiz' : 'o',
'split_vert' : 'e',
'close_term' : 'w',
'copy' : 'c',
'paste' : 'v',
'toggle_scrollbar' : 's',
'search' : 'f',
'page_up' : '',
'page_down' : '',
'page_up_half' : '',
'page_down_half' : '',
'line_up' : '',
'line_down' : '',
'close_window' : 'q',
'resize_up' : 'Up',
'resize_down' : 'Down',
'resize_left' : 'Left',
'resize_right' : 'Right',
'move_tab_right' : 'Page_Down',
'move_tab_left' : 'Page_Up',
'toggle_zoom' : 'x',
'scaled_zoom' : 'z',
'next_tab' : 'Page_Down',
'prev_tab' : 'Page_Up',
'switch_to_tab_1' : '',
'switch_to_tab_2' : '',
'switch_to_tab_3' : '',
'switch_to_tab_4' : '',
'switch_to_tab_5' : '',
'switch_to_tab_6' : '',
'switch_to_tab_7' : '',
'switch_to_tab_8' : '',
'switch_to_tab_9' : '',
'switch_to_tab_10' : '',
'full_screen' : 'F11',
'reset' : 'r',
'reset_clear' : 'g',
'hide_window' : 'a',
'group_all' : 'g',
'group_all_toggle' : '',
'ungroup_all' : 'g',
'group_tab' : 't',
'group_tab_toggle' : '',
'ungroup_tab' : 't',
'new_window' : 'i',
'new_terminator' : 'i',
'broadcast_off' : 'o',
'broadcast_group' : 'g',
'broadcast_all' : 'a',
'insert_number' : '1',
'insert_padded' : '0',
'edit_window_title': 'w',
'edit_tab_title' : 'a',
'edit_terminal_title': 'x',
'layout_launcher' : 'l',
'next_profile' : '',
'previous_profile' : '',
'help' : 'F1'
},
'profiles': {
'default': {
'allow_bold' : True,
'audible_bell' : False,
'visible_bell' : False,
'urgent_bell' : False,
'icon_bell' : True,
'background_color' : '#000000',
'background_darkness' : 0.5,
'background_type' : 'solid',
'backspace_binding' : 'ascii-del',
'delete_binding' : 'escape-sequence',
'color_scheme' : 'grey_on_black',
'cursor_blink' : True,
'cursor_shape' : 'block',
'cursor_color' : '',
'cursor_color_fg' : True,
'term' : 'xterm-256color',
'colorterm' : 'truecolor',
'font' : 'Mono 10',
'foreground_color' : '#aaaaaa',
'show_titlebar' : True,
'scrollbar_position' : "right",
'scroll_background' : True,
'scroll_on_keystroke' : True,
'scroll_on_output' : False,
'scrollback_lines' : 500,
'scrollback_infinite' : False,
'exit_action' : 'close',
'palette' : '#2e3436:#cc0000:#4e9a06:#c4a000:\
#3465a4:#75507b:#06989a:#d3d7cf:#555753:#ef2929:#8ae234:#fce94f:\
#729fcf:#ad7fa8:#34e2e2:#eeeeec',
'word_chars' : '-,./?%:_',
'mouse_autohide' : True,
'login_shell' : False,
'use_custom_command' : False,
'custom_command' : '',
'use_system_font' : True,
'use_theme_colors' : False,
'encoding' : 'UTF-8',
'active_encodings' : ['UTF-8', 'ISO-8859-1'],
'focus_on_close' : 'auto',
'force_no_bell' : False,
'cycle_term_tab' : True,
'copy_on_selection' : False,
'rewrap_on_resize' : True,
'split_to_group' : False,
'autoclean_groups' : True,
'http_proxy' : '',
'ignore_hosts' : ['localhost','127.0.0.0/8','*.local'],
},
},
'layouts': {
'default': {
'window0': {
'type': 'Window',
'parent': ''
},
'child1': {
'type': 'Terminal',
'parent': 'window0'
}
}
},
'plugins': {
},
}
class Config(object):
"""Class to provide a slightly richer config API above ConfigBase"""
base = None
profile = None
system_mono_font = None
system_prop_font = None
system_focus = None
inhibited = None
def __init__(self, profile='default'):
self.base = ConfigBase()
self.set_profile(profile)
self.inhibited = False
self.connect_gsetting_callbacks()
def __getitem__(self, key, default=None):
"""Look up a configuration item"""
return(self.base.get_item(key, self.profile, default=default))
def __setitem__(self, key, value):
"""Set a particular configuration item"""
return(self.base.set_item(key, value, self.profile))
def get_profile(self):
"""Get our profile"""
return(self.profile)
def set_profile(self, profile, force=False):
"""Set our profile (which usually means change it)"""
options = self.options_get()
if not force and options and options.profile and profile == 'default':
dbg('overriding default profile to %s' % options.profile)
profile = options.profile
dbg('Config::set_profile: Changing profile to %s' % profile)
self.profile = profile
if not self.base.profiles.has_key(profile):
dbg('Config::set_profile: %s does not exist, creating' % profile)
self.base.profiles[profile] = copy(DEFAULTS['profiles']['default'])
def add_profile(self, profile):
"""Add a new profile"""
return(self.base.add_profile(profile))
def del_profile(self, profile):
"""Delete a profile"""
if profile == self.profile:
# FIXME: We should solve this problem by updating terminals when we
# remove a profile
err('Config::del_profile: Deleting in-use profile %s.' % profile)
self.set_profile('default')
if self.base.profiles.has_key(profile):
del(self.base.profiles[profile])
options = self.options_get()
if options and options.profile == profile:
options.profile = None
self.options_set(options)
def rename_profile(self, profile, newname):
"""Rename a profile"""
if self.base.profiles.has_key(profile):
self.base.profiles[newname] = self.base.profiles[profile]
del(self.base.profiles[profile])
if profile == self.profile:
self.profile = newname
def list_profiles(self):
"""List all configured profiles"""
return(self.base.profiles.keys())
def add_layout(self, name, layout):
"""Add a new layout"""
return(self.base.add_layout(name, layout))
def replace_layout(self, name, layout):
"""Replace an existing layout"""
return(self.base.replace_layout(name, layout))
def del_layout(self, layout):
"""Delete a layout"""
if self.base.layouts.has_key(layout):
del(self.base.layouts[layout])
def rename_layout(self, layout, newname):
"""Rename a layout"""
if self.base.layouts.has_key(layout):
self.base.layouts[newname] = self.base.layouts[layout]
del(self.base.layouts[layout])
def list_layouts(self):
"""List all configured layouts"""
return(self.base.layouts.keys())
def connect_gsetting_callbacks(self):
"""Get system settings and create callbacks for changes"""
dbg("GSetting connects for system changes")
# Have to preserve these to self, or callbacks don't happen
self.gsettings_interface=Gio.Settings.new('org.gnome.desktop.interface')
self.gsettings_interface.connect("changed::font-name", self.on_gsettings_change_event)
self.gsettings_interface.connect("changed::monospace-font-name", self.on_gsettings_change_event)
self.gsettings_wm=Gio.Settings.new('org.gnome.desktop.wm.preferences')
self.gsettings_wm.connect("changed::focus-mode", self.on_gsettings_change_event)
def get_system_prop_font(self):
"""Look up the system font"""
if self.system_prop_font is not None:
return(self.system_prop_font)
elif 'org.gnome.desktop.interface' not in Gio.Settings.list_schemas():
return
else:
gsettings=Gio.Settings.new('org.gnome.desktop.interface')
value = gsettings.get_value('font-name')
if value:
self.system_prop_font = value.get_string()
else:
self.system_prop_font = "Sans 10"
return(self.system_prop_font)
def get_system_mono_font(self):
"""Look up the system font"""
if self.system_mono_font is not None:
return(self.system_mono_font)
elif 'org.gnome.desktop.interface' not in Gio.Settings.list_schemas():
return
else:
gsettings=Gio.Settings.new('org.gnome.desktop.interface')
value = gsettings.get_value('monospace-font-name')
if value:
self.system_mono_font = value.get_string()
else:
self.system_mono_font = "Mono 10"
return(self.system_mono_font)
def get_system_focus(self):
"""Look up the system focus setting"""
if self.system_focus is not None:
return(self.system_focus)
elif 'org.gnome.desktop.interface' not in Gio.Settings.list_schemas():
return
else:
gsettings=Gio.Settings.new('org.gnome.desktop.wm.preferences')
value = gsettings.get_value('focus-mode')
if value:
self.system_focus = value.get_string()
return(self.system_focus)
def on_gsettings_change_event(self, settings, key):
"""Handle a gsetting change event"""
dbg('GSetting change event received. Invalidating caches')
self.system_focus = None
self.system_font = None
self.system_mono_font = None
# Need to trigger a reconfigure to change active terminals immediately
if "Terminator" not in globals():
from terminator import Terminator
Terminator().reconfigure()
def save(self):
"""Cause ConfigBase to save our config to file"""
if self.inhibited is True:
return(True)
else:
return(self.base.save())
def inhibit_save(self):
"""Prevent calls to save() being honoured"""
self.inhibited = True
def uninhibit_save(self):
"""Allow calls to save() to be honoured"""
self.inhibited = False
def options_set(self, options):
"""Set the command line options"""
self.base.command_line_options = options
def options_get(self):
"""Get the command line options"""
return(self.base.command_line_options)
def plugin_get(self, pluginname, key, default=None):
"""Get a plugin config value, if doesn't exist
return default if specified
"""
return(self.base.get_item(key, plugin=pluginname, default=default))
def plugin_set(self, pluginname, key, value):
"""Set a plugin config value"""
return(self.base.set_item(key, value, plugin=pluginname))
def plugin_get_config(self, plugin):
"""Return a whole config tree for a given plugin"""
return(self.base.get_plugin(plugin))
def plugin_set_config(self, plugin, tree):
"""Set a whole config tree for a given plugin"""
return(self.base.set_plugin(plugin, tree))
def plugin_del_config(self, plugin):
"""Delete a whole config tree for a given plugin"""
return(self.base.del_plugin(plugin))
def layout_get_config(self, layout):
"""Return a layout"""
return(self.base.get_layout(layout))
def layout_set_config(self, layout, tree):
"""Set a layout"""
return(self.base.set_layout(layout, tree))
class ConfigBase(Borg):
"""Class to provide access to our user configuration"""
loaded = None
whined = None
sections = None
global_config = None
profiles = None
keybindings = None
plugins = None
layouts = None
command_line_options = None
def __init__(self):
"""Class initialiser"""
Borg.__init__(self, self.__class__.__name__)
self.prepare_attributes()
import optionparse
self.command_line_options = optionparse.options
self.load()
def prepare_attributes(self):
"""Set up our borg environment"""
if self.loaded is None:
self.loaded = False
if self.whined is None:
self.whined = False
if self.sections is None:
self.sections = ['global_config', 'keybindings', 'profiles',
'layouts', 'plugins']
if self.global_config is None:
self.global_config = copy(DEFAULTS['global_config'])
if self.profiles is None:
self.profiles = {}
self.profiles['default'] = copy(DEFAULTS['profiles']['default'])
if self.keybindings is None:
self.keybindings = copy(DEFAULTS['keybindings'])
if self.plugins is None:
self.plugins = {}
if self.layouts is None:
self.layouts = {}
for layout in DEFAULTS['layouts']:
self.layouts[layout] = copy(DEFAULTS['layouts'][layout])
def defaults_to_configspec(self):
"""Convert our tree of default values into a ConfigObj validation
specification"""
configspecdata = {}
keymap = {
'int': 'integer',
'str': 'string',
'bool': 'boolean',
}
section = {}
for key in DEFAULTS['global_config']:
keytype = DEFAULTS['global_config'][key].__class__.__name__
value = DEFAULTS['global_config'][key]
if keytype in keymap:
keytype = keymap[keytype]
elif keytype == 'list':
value = 'list(%s)' % ','.join(value)
keytype = '%s(default=%s)' % (keytype, value)
if key == 'custom_url_handler':
keytype = 'string(default="")'
section[key] = keytype
configspecdata['global_config'] = section
section = {}
for key in DEFAULTS['keybindings']:
value = DEFAULTS['keybindings'][key]
if value is None or value == '':
continue
section[key] = 'string(default=%s)' % value
configspecdata['keybindings'] = section
section = {}
for key in DEFAULTS['profiles']['default']:
keytype = DEFAULTS['profiles']['default'][key].__class__.__name__
value = DEFAULTS['profiles']['default'][key]
if keytype in keymap:
keytype = keymap[keytype]
elif keytype == 'list':
value = 'list(%s)' % ','.join(value)
if keytype == 'string':
value = '"%s"' % value
keytype = '%s(default=%s)' % (keytype, value)
section[key] = keytype
configspecdata['profiles'] = {}
configspecdata['profiles']['__many__'] = section
section = {}
section['type'] = 'string'
section['parent'] = 'string'
section['profile'] = 'string(default=default)'
section['command'] = 'string(default="")'
section['position'] = 'string(default="")'
section['size'] = 'list(default=list(-1,-1))'
configspecdata['layouts'] = {}
configspecdata['layouts']['__many__'] = {}
configspecdata['layouts']['__many__']['__many__'] = section
configspecdata['plugins'] = {}
configspec = ConfigObj(configspecdata)
if DEBUG == True:
configspec.write(open('/tmp/terminator_configspec_debug.txt', 'w'))
return(configspec)
def load(self):
"""Load configuration data from our various sources"""
if self.loaded is True:
dbg('ConfigBase::load: config already loaded')
return
if self.command_line_options:
if not self.command_line_options.config:
self.command_line_options.config = os.path.join(get_config_dir(), 'config')
filename = self.command_line_options.config
else:
filename = os.path.join(get_config_dir(), 'config')
dbg('looking for config file: %s' % filename)
try:
configfile = open(filename, 'r')
except Exception, ex:
if not self.whined:
err('ConfigBase::load: Unable to open %s (%s)' % (filename, ex))
self.whined = True
return
# If we have successfully loaded a config, allow future whining
self.whined = False
try:
configspec = self.defaults_to_configspec()
parser = ConfigObj(configfile, configspec=configspec)
validator = Validator()
result = parser.validate(validator, preserve_errors=True)
except Exception, ex:
err('Unable to load configuration: %s' % ex)
return
if result != True:
err('ConfigBase::load: config format is not valid')
for (section_list, key, _other) in flatten_errors(parser, result):
if key is not None:
err('[%s]: %s is invalid' % (','.join(section_list), key))
else:
err('[%s] missing' % ','.join(section_list))
else:
dbg('config validated successfully')
for section_name in self.sections:
dbg('ConfigBase::load: Processing section: %s' % section_name)
section = getattr(self, section_name)
if section_name == 'profiles':
for profile in parser[section_name]:
dbg('ConfigBase::load: Processing profile: %s' % profile)
if not section.has_key(section_name):
# FIXME: Should this be outside the loop?
section[profile] = copy(DEFAULTS['profiles']['default'])
section[profile].update(parser[section_name][profile])
elif section_name == 'plugins':
if not parser.has_key(section_name):
continue
for part in parser[section_name]:
dbg('ConfigBase::load: Processing %s: %s' % (section_name,
part))
section[part] = parser[section_name][part]
elif section_name == 'layouts':
for layout in parser[section_name]:
dbg('ConfigBase::load: Processing %s: %s' % (section_name,
layout))
if layout == 'default' and \
parser[section_name][layout] == {}:
continue
section[layout] = parser[section_name][layout]
elif section_name == 'keybindings':
if not parser.has_key(section_name):
continue
for part in parser[section_name]:
dbg('ConfigBase::load: Processing %s: %s' % (section_name,
part))
if parser[section_name][part] == 'None':
section[part] = None
else:
section[part] = parser[section_name][part]
else:
try:
section.update(parser[section_name])
except KeyError, ex:
dbg('ConfigBase::load: skipping missing section %s' %
section_name)
self.loaded = True
def reload(self):
"""Force a reload of the base config"""
self.loaded = False
self.load()
def save(self):
"""Save the config to a file"""
dbg('ConfigBase::save: saving config')
parser = ConfigObj()
parser.indent_type = ' '
for section_name in ['global_config', 'keybindings']:
dbg('ConfigBase::save: Processing section: %s' % section_name)
section = getattr(self, section_name)
parser[section_name] = dict_diff(DEFAULTS[section_name], section)
parser['profiles'] = {}
for profile in self.profiles:
dbg('ConfigBase::save: Processing profile: %s' % profile)
parser['profiles'][profile] = dict_diff(
DEFAULTS['profiles']['default'], self.profiles[profile])
parser['layouts'] = {}
for layout in self.layouts:
dbg('ConfigBase::save: Processing layout: %s' % layout)
parser['layouts'][layout] = self.layouts[layout]
parser['plugins'] = {}
for plugin in self.plugins:
dbg('ConfigBase::save: Processing plugin: %s' % plugin)
parser['plugins'][plugin] = self.plugins[plugin]
config_dir = get_config_dir()
if not os.path.isdir(config_dir):
os.makedirs(config_dir)
try:
parser.write(open(self.command_line_options.config, 'w'))
except Exception, ex:
err('ConfigBase::save: Unable to save config: %s' % ex)
def get_item(self, key, profile='default', plugin=None, default=None):
"""Look up a configuration item"""
if not self.profiles.has_key(profile):
# Hitting this generally implies a bug
profile = 'default'
if self.global_config.has_key(key):
dbg('ConfigBase::get_item: %s found in globals: %s' %
(key, self.global_config[key]))
return(self.global_config[key])
elif self.profiles[profile].has_key(key):
dbg('ConfigBase::get_item: %s found in profile %s: %s' % (
key, profile, self.profiles[profile][key]))
return(self.profiles[profile][key])
elif key == 'keybindings':
return(self.keybindings)
elif plugin and plugin in self.plugins and key in self.plugins[plugin]:
dbg('ConfigBase::get_item: %s found in plugin %s: %s' % (
key, plugin, self.plugins[plugin][key]))
return(self.plugins[plugin][key])
elif default:
return default
else:
raise KeyError('ConfigBase::get_item: unknown key %s' % key)
def set_item(self, key, value, profile='default', plugin=None):
"""Set a configuration item"""
dbg('ConfigBase::set_item: Setting %s=%s (profile=%s, plugin=%s)' %
(key, value, profile, plugin))
if self.global_config.has_key(key):
self.global_config[key] = value
elif self.profiles[profile].has_key(key):
self.profiles[profile][key] = value
elif key == 'keybindings':
self.keybindings = value
elif plugin is not None:
if not self.plugins.has_key(plugin):
self.plugins[plugin] = {}
self.plugins[plugin][key] = value
else:
raise KeyError('ConfigBase::set_item: unknown key %s' % key)
return(True)
def get_plugin(self, plugin):
"""Return a whole tree for a plugin"""
if self.plugins.has_key(plugin):
return(self.plugins[plugin])
def set_plugin(self, plugin, tree):
"""Set a whole tree for a plugin"""
self.plugins[plugin] = tree
def del_plugin(self, plugin):
"""Delete a whole tree for a plugin"""
if plugin in self.plugins:
del self.plugins[plugin]
def add_profile(self, profile):
"""Add a new profile"""
if profile in self.profiles:
return(False)
self.profiles[profile] = copy(DEFAULTS['profiles']['default'])
return(True)
def add_layout(self, name, layout):
"""Add a new layout"""
if name in self.layouts:
return(False)
self.layouts[name] = layout
return(True)
def replace_layout(self, name, layout):
"""Replaces a layout with the given name"""
if not name in self.layouts:
return(False)
self.layouts[name] = layout
return(True)
def get_layout(self, layout):
"""Return a layout"""
if self.layouts.has_key(layout):
return(self.layouts[layout])
else:
err('layout does not exist: %s' % layout)
def set_layout(self, layout, tree):
"""Set a layout"""
self.layouts[layout] = tree
terminator-1.91/terminatorlib/plugins/ 0000775 0001750 0001750 00000000000 13055372500 020464 5 ustar steve steve 0000000 0000000 terminator-1.91/terminatorlib/plugins/custom_commands.py 0000775 0001750 0001750 00000040406 13054612071 024237 0 ustar steve steve 0000000 0000000 #!/usr/bin/env python2
# Terminator by Chris Jones
# GPL v2 only
"""custom_commands.py - Terminator Plugin to add custom command menu entries"""
import sys
import os
# Fix imports when testing this file directly
if __name__ == '__main__':
sys.path.append( os.path.join(os.path.dirname(__file__), "../.."))
from gi.repository import Gtk
from gi.repository import GObject
import terminatorlib.plugin as plugin
from terminatorlib.config import Config
from terminatorlib.translation import _
from terminatorlib.util import get_config_dir, err, dbg, gerr
(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
noord_cmds = []
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
if s.has_key("position"):
self.cmd_list[int(s["position"])] = {'enabled' : enabled,
'name' : name,
'command' : command
}
else:
noord_cmds.append(
{'enabled' : enabled,
'name' : name,
'command' : command
}
)
for cmd in noord_cmds:
self.cmd_list[len(self.cmd_list)] = cmd
def callback(self, menuitems, menu, terminal):
"""Add our menu items to the menu"""
submenus = {}
item = Gtk.MenuItem.new_with_mnemonic(_('_Custom Commands'))
menuitems.append(item)
submenu = Gtk.Menu()
item.set_submenu(submenu)
menuitem = Gtk.MenuItem.new_with_mnemonic(_('_Preferences'))
menuitem.connect("activate", self.configure)
submenu.append(menuitem)
menuitem = Gtk.SeparatorMenuItem()
submenu.append(menuitem)
theme = Gtk.IconTheme.get_default()
for command in [ self.cmd_list[key] for key in sorted(self.cmd_list.keys()) ] :
if not command['enabled']:
continue
exe = command['command'].split(' ')[0]
iconinfo = theme.choose_icon([exe], Gtk.IconSize.MENU, Gtk.IconLookupFlags.USE_BUILTIN)
leaf_name = command['name'].split('/')[-1]
branch_names = command['name'].split('/')[:-1]
target_submenu = submenu
parent_submenu = submenu
for idx in range(len(branch_names)):
lookup_name = '/'.join(branch_names[0:idx+1])
target_submenu = submenus.get(lookup_name, None)
if not target_submenu:
item = Gtk.MenuItem(_(branch_names[idx]))
parent_submenu.append(item)
target_submenu = Gtk.Menu()
item.set_submenu(target_submenu)
submenus[lookup_name] = target_submenu
parent_submenu = target_submenu
if iconinfo:
image = Gtk.Image()
image.set_from_icon_name(exe, Gtk.IconSize.MENU)
menuitem = Gtk.ImageMenuItem(leaf_name)
menuitem.set_image(image)
else:
menuitem = Gtk.MenuItem(leaf_name)
terminals = terminal.terminator.get_target_terms(terminal)
menuitem.connect("activate", self._execute, {'terminals' : terminals, 'command' : command['command'] })
target_submenu.append(menuitem)
def _save_config(self):
config = Config()
config.plugin_del_config(self.__class__.__name__)
i = 0
for command in [ self.cmd_list[key] for key in sorted(self.cmd_list.keys()) ] :
enabled = command['enabled']
name = command['name']
command = command['command']
item = {}
item['enabled'] = enabled
item['name'] = name
item['command'] = command
item['position'] = i
config.plugin_set(self.__class__.__name__, name, item)
i = i + 1
config.save()
def _execute(self, widget, data):
command = data['command']
if command[-1] != '\n':
command = command + '\n'
for terminal in data['terminals']:
terminal.vte.feed_child(command, len(command))
def configure(self, widget, data = None):
ui = {}
dbox = Gtk.Dialog(
_("Custom Commands Configuration"),
None,
Gtk.DialogFlags.MODAL,
(
_("_Cancel"), Gtk.ResponseType.REJECT,
_("_OK"), Gtk.ResponseType.ACCEPT
)
)
dbox.set_transient_for(widget.get_toplevel())
icon_theme = Gtk.IconTheme.get_default()
if icon_theme.lookup_icon('terminator-custom-commands', 48, 0):
dbox.set_icon_name('terminator-custom-commands')
else:
dbg('Unable to load Terminator custom command icon')
icon = dbox.render_icon(Gtk.STOCK_DIALOG_INFO, Gtk.IconSize.BUTTON)
dbox.set_icon(icon)
store = Gtk.ListStore(bool, str, str)
for command in [ self.cmd_list[key] for key in sorted(self.cmd_list.keys()) ]:
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.SelectionMode.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)
scroll_window = Gtk.ScrolledWindow()
scroll_window.set_size_request(500, 250)
scroll_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
scroll_window.add_with_viewport(treeview)
hbox = Gtk.HBox()
hbox.pack_start(scroll_window, True, True, 0)
dbox.vbox.pack_start(hbox, True, True, 0)
button_box = Gtk.VBox()
button = Gtk.Button(_("Top"))
button_box.pack_start(button, False, True, 0)
button.connect("clicked", self.on_goto_top, ui)
button.set_sensitive(False)
ui['button_top'] = button
button = Gtk.Button(_("Up"))
button_box.pack_start(button, False, True, 0)
button.connect("clicked", self.on_go_up, ui)
button.set_sensitive(False)
ui['button_up'] = button
button = Gtk.Button(_("Down"))
button_box.pack_start(button, False, True, 0)
button.connect("clicked", self.on_go_down, ui)
button.set_sensitive(False)
ui['button_down'] = button
button = Gtk.Button(_("Last"))
button_box.pack_start(button, False, True, 0)
button.connect("clicked", self.on_goto_last, ui)
button.set_sensitive(False)
ui['button_last'] = button
button = Gtk.Button(_("New"))
button_box.pack_start(button, False, True, 0)
button.connect("clicked", self.on_new, ui)
ui['button_new'] = button
button = Gtk.Button(_("Edit"))
button_box.pack_start(button, False, True, 0)
button.set_sensitive(False)
button.connect("clicked", self.on_edit, ui)
ui['button_edit'] = button
button = Gtk.Button(_("Delete"))
button_box.pack_start(button, False, True, 0)
button.connect("clicked", self.on_delete, ui)
button.set_sensitive(False)
ui['button_delete'] = button
hbox.pack_start(button_box, False, True, 0)
self.dbox = dbox
dbox.show_all()
res = dbox.run()
if res == Gtk.ResponseType.ACCEPT:
self.update_cmd_list(store)
self._save_config()
del(self.dbox)
dbox.destroy()
return
def update_cmd_list(self, store):
iter = store.get_iter_first()
self.cmd_list = {}
i=0
while iter:
(enabled, name, command) = store.get(iter,
CC_COL_ENABLED,
CC_COL_NAME,
CC_COL_COMMAND)
self.cmd_list[i] = {'enabled' : enabled,
'name': name,
'command' : command}
iter = store.iter_next(iter)
i = i + 1
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)
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.DialogFlags.MODAL,
(
_("_Cancel"), Gtk.ResponseType.REJECT,
_("_OK"), Gtk.ResponseType.ACCEPT
)
)
dialog.set_transient_for(self.dbox)
table = Gtk.Table(3, 2)
label = Gtk.Label(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(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(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, True, True, 0)
dialog.show_all()
return (dialog,enabled,name,command)
def on_new(self, button, data):
(dialog,enabled,name,command) = self._create_command_dialog()
res = dialog.run()
item = {}
if res == Gtk.ResponseType.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.DialogFlags.MODAL,
Gtk.MessageType.ERROR,
Gtk.ButtonsType.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:
gerr(_("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.ResponseType.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.DialogFlags.MODAL,
Gtk.MessageType.ERROR,
Gtk.ButtonsType.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:
gerr(_("Name *%s* already exist") % item['name'])
dialog.destroy()
if __name__ == '__main__':
c = CustomCommandsMenu()
c.configure(None, None)
Gtk.main()
terminator-1.91/terminatorlib/plugins/maven.py 0000775 0001750 0001750 00000010766 13054612071 022160 0 ustar steve steve 0000000 0000000 #!/usr/bin/env python2
# Copyright (c) 2010 Julien Nicoulaud
#
# 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
import re
import terminatorlib.plugin as plugin
AVAILABLE = [ 'MavenPluginURLHandler' ]
class MavenPluginURLHandler(plugin.URLHandler):
"""Maven plugin handler. If the name of a Maven plugin is
detected, it is turned into a link to its documentation site.
If a Maven plugin goal is detected, the link points to the
particular goal page. Only Apache (org.apache.maven.plugins)
and Codehaus (org.codehaus.mojo) plugins are supported."""
capabilities = ['url_handler']
handler_name = 'maven_plugin'
maven_filters = {}
maven_filters['apache_maven_plugin_shortname'] = 'clean|compiler|deploy|failsafe|install|resources|site|surefire|verifier|ear|ejb|jar|rar|war|shade|changelog|changes|checkstyle|clover|doap|docck|javadoc|jxr|linkcheck|pmd|project-info-reports|surefire-report|ant|antrun|archetype|assembly|dependency|enforcer|gpg|help|invoker|jarsigner|one|patch|pdf|plugin|release|reactor|remote-resources|repository|scm|source|stage|toolchains|eclipse|idea'
maven_filters['codehaus_maven_plugin_shortname'] = 'jboss|jboss-packaging|tomcat|was6|antlr|antlr3|aspectj|axistools|castor|commons-attributes|gwt|hibernate3|idlj|javacc|jaxb2|jpox|jspc|openjpa|rmic|sablecc|sqlj|sysdeo-tomcat|xmlbeans|xdoclet|netbeans-freeform|nbm|clirr|cobertura|taglist|scmchangelog|findbugs|fitnesse|selenium|animal-sniffer|appassembler|build-helper|exec|keytool|latex|ounce|rpm|sql|versions|apt|cbuilds|jspc|native|retrotranslator|springws|smc|ideauidesigner|dita|docbook|javancss|jdepend|dashboard|emma|sonar|jruby|dbunit|shitty|batik|buildnumber|ianal|jalopy|jasperreports|l10n|minijar|native2ascii|osxappbundle|properties|solaris|truezip|unix|virtualization|wagon|webstart|xml|argouml|dbupgrade|chronos|ckjm|codenarc|deb|ec2|enchanter|ejbdoclet|graphing|j2me|javascript tools|jardiff|kodo|macker|springbeandoc|mock-repository|nsis|pomtools|setup|simian-report|syslog|visibroker|weblogic|webdoclet|xjc|xsltc'
maven_filters['apache_maven_plugin_artifact_id'] = 'maven\-(%(apache_maven_plugin_shortname)s)\-plugin' % maven_filters
maven_filters['codehaus_maven_plugin_artifact_id'] = '(%(codehaus_maven_plugin_shortname)s)\-maven\-plugin' % maven_filters
maven_filters['maven_plugin_version'] = '[a-zA-Z0-9\.-]+'
maven_filters['maven_plugin_goal'] = '[a-zA-Z-]+'
maven_filters['maven_plugin'] = '(%(apache_maven_plugin_artifact_id)s|%(codehaus_maven_plugin_artifact_id)s)(:%(maven_plugin_version)s:%(maven_plugin_goal)s)?' % maven_filters
maven_filters['maven_plugin_named_groups'] = '(?P%(apache_maven_plugin_artifact_id)s|%(codehaus_maven_plugin_artifact_id)s)(:%(maven_plugin_version)s:(?P%(maven_plugin_goal)s))?' % maven_filters
match = '\\b%(maven_plugin)s\\b' % maven_filters
def callback(self, url):
matches = re.match(MavenPluginURLHandler.maven_filters['maven_plugin_named_groups'], url)
if matches is not None:
artifactid = matches.group('artifact_id')
goal = matches.group('goal')
if artifactid is not None:
if re.match(MavenPluginURLHandler.maven_filters['apache_maven_plugin_artifact_id'], artifactid):
if goal is not None:
return 'http://maven.apache.org/plugins/%s/%s-mojo.html' % ( artifactid, goal)
else:
return 'http://maven.apache.org/plugins/%s' % artifactid
elif re.match(MavenPluginURLHandler.maven_filters['codehaus_maven_plugin_artifact_id'], artifactid):
if goal is not None:
return 'http://mojo.codehaus.org/%s/%s-mojo.html' % ( artifactid, goal)
else:
return 'http://mojo.codehaus.org/%s' % artifactid
plugin.err("Wrong match '%s' for MavenPluginURLHandler." % url)
return ''
terminator-1.91/terminatorlib/plugins/url_handlers.py 0000664 0001750 0001750 00000004502 13054612071 023520 0 ustar steve steve 0000000 0000000 #!/usr/bin/env python2
# Terminator by Chris Jones