terminator/terminatorlib/terminal.py
2011-08-20 14:53:23 +01:00

1543 lines
55 KiB
Python
Executable File

#!/usr/bin/python
# Terminator by Chris Jones <cmsj@tenshu.net>
# GPL v2 only
"""terminal.py - classes necessary to provide Terminal widgets"""
from __future__ import division
import sys
import os
import signal
import pygtk
pygtk.require('2.0')
import gtk
import gobject
import pango
import subprocess
import urllib
from util import dbg, err, gerr
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
try:
import vte
except ImportError:
gerr('You need to install python bindings for libvte')
sys.exit(1)
# pylint: disable-msg=R0904
class Terminal(gtk.VBox):
"""Class implementing the VTE widget and its wrappings"""
__gsignals__ = {
'close-term': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
'title-change': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
(gobject.TYPE_STRING,)),
'enumerate': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
(gobject.TYPE_INT,)),
'group-tab': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
'ungroup-tab': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
'ungroup-all': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
'split-horiz': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
(gobject.TYPE_STRING,)),
'split-vert': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
(gobject.TYPE_STRING,)),
'tab-new': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
(gobject.TYPE_BOOLEAN, gobject.TYPE_OBJECT)),
'tab-top-new': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
'focus-in': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
'zoom': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
'maximise': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
'unzoom': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
'resize-term': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
(gobject.TYPE_STRING,)),
'navigate': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
(gobject.TYPE_STRING,)),
'tab-change': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
(gobject.TYPE_INT,)),
'group-all': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
'move-tab': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
(gobject.TYPE_STRING,)),
}
TARGET_TYPE_VTE = 8
terminator = None
vte = None
terminalbox = None
scrollbar = None
scrollbar_position = None
titlebar = None
searchbar = None
group = None
cwd = None
origcwd = None
command = None
clipboard = None
pid = None
matches = None
config = None
default_encoding = None
custom_encoding = None
custom_font_size = None
layout_command = None
composite_support = None
cnxids = None
def __init__(self):
"""Class initialiser"""
gtk.VBox.__init__(self)
self.__gobject_init__()
self.terminator = Terminator()
self.terminator.register_terminal(self)
# 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.matches = {}
self.cnxids = Signalman()
self.config = Config()
self.cwd = get_default_cwd()
self.origcwd = self.terminator.origcwd
self.clipboard = gtk.clipboard_get(gtk.gdk.SELECTION_CLIPBOARD)
self.pending_on_vte_size_allocate = False
self.vte = vte.Terminal()
self.vte.set_size(80, 24)
self.vte._expose_data = None
if not hasattr(self.vte, "set_opacity") or \
not hasattr(self.vte, "is_composited"):
self.composite_support = False
else:
self.composite_support = True
dbg('composite_support: %s' % self.composite_support)
self.vte.show()
self.default_encoding = self.vte.get_encoding()
self.update_url_matches(self.config['try_posix_regexp'])
self.terminalbox = self.create_terminalbox()
self.titlebar = Titlebar(self)
self.titlebar.connect_icon(self.on_group_button_press)
self.titlebar.connect('edit-done', self.on_edit_done)
self.connect('title-change', self.titlebar.set_terminal_title)
self.titlebar.connect('create-group', self.really_create_group)
self.titlebar.show_all()
self.searchbar = Searchbar()
self.searchbar.connect('end-search', self.on_search_done)
self.show()
self.pack_start(self.titlebar, False)
self.pack_start(self.terminalbox)
self.pack_end(self.searchbar)
self.connect_signals()
os.putenv('TERM', 'xterm')
os.putenv('COLORTERM', 'gnome-terminal')
env_proxy = os.getenv('http_proxy')
if not env_proxy:
if self.config['http_proxy'] and self.config['http_proxy'] != '':
os.putenv('http_proxy', self.config['http_proxy'])
def 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 get_cwd(self):
"""Return our cwd"""
return(self.terminator.pid_cwd(self.pid))
def close(self):
"""Close ourselves"""
dbg('Terminal::close: emitting close-term')
self.emit('close-term')
try:
os.kill(self.pid, signal.SIGHUP)
except:
# We really don't want to care if this failed. Deep OS voodoo is
# not what we should be doing.
pass
def create_terminalbox(self):
"""Create a GtkHBox containing the terminal and a scrollbar"""
terminalbox = gtk.HBox()
self.scrollbar = gtk.VScrollbar(self.vte.get_adjustment())
self.scrollbar.set_no_show_all(True)
self.scrollbar_position = self.config['scrollbar_position']
if self.scrollbar_position not in ('hidden', 'disabled'):
self.scrollbar.show()
if self.scrollbar_position == 'left':
func = terminalbox.pack_end
else:
func = terminalbox.pack_start
func(self.vte)
func(self.scrollbar, False)
terminalbox.show_all()
return(terminalbox)
def update_url_matches(self, posix = True):
"""Update the regexps used to match URLs"""
userchars = "-A-Za-z0-9"
passchars = "-A-Za-z0-9,?;.:/!%$^*&~\"#'"
hostchars = "-A-Za-z0-9"
pathchars = "-A-Za-z0-9_$.+!*(),;:@&=?/~#%'\""
schemes = "(news:|telnet:|nntp:|file:/|https?:|ftps?:|webcal:)"
user = "[" + userchars + "]+(:[" + passchars + "]+)?"
urlpath = "/[" + pathchars + "]*[^]'.}>) \t\r\n,\\\"]"
if posix:
dbg ('Terminal::update_url_matches: Trying POSIX URL regexps')
lboundry = "[[:<:]]"
rboundry = "[[:>:]]"
else: # GNU
dbg ('Terminal::update_url_matches: Trying GNU URL regexps')
lboundry = "\\<"
rboundry = "\\>"
self.matches['full_uri'] = self.vte.match_add(lboundry + schemes +
"//(" + user + "@)?[" + hostchars +".]+(:[0-9]+)?(" +
urlpath + ")?" + rboundry + "/?")
if self.matches['full_uri'] == -1:
if posix:
err ('Terminal::update_url_matches: POSIX failed, trying GNU')
self.update_url_matches(posix = False)
else:
err ('Terminal::update_url_matches: Failed adding URL matches')
else:
self.matches['voip'] = self.vte.match_add(lboundry +
'(callto:|h323:|sip:)' + "[" + userchars + "+][" +
userchars + ".]*(:[0-9]+)?@?[" + pathchars + "]+" +
rboundry)
self.matches['addr_only'] = self.vte.match_add (lboundry +
"(www|ftp)[" + hostchars + "]*\.[" + hostchars +
".]+(:[0-9]+)?(" + urlpath + ")?" + rboundry + "/?")
self.matches['email'] = self.vte.match_add (lboundry +
"(mailto:)?[a-zA-Z0-9][a-zA-Z0-9.+-]*@[a-zA-Z0-9]" +
"[a-zA-Z0-9-]*\.[a-zA-Z0-9][a-zA-Z0-9-]+" +
"[.a-zA-Z0-9-]*" + rboundry)
self.matches['nntp'] = self.vte.match_add (lboundry +
"""news:[-A-Z\^_a-z{|}~!"#$%&'()*+,./0-9;:=?`]+@""" +
"[-A-Za-z0-9.]+(:[0-9]+)?" + rboundry)
# Now add any matches from plugins
try:
registry = plugin.PluginRegistry()
registry.load_plugins()
plugins = registry.get_plugins_by_capability('url_handler')
for urlplugin in plugins:
name = urlplugin.handler_name
match = urlplugin.match
if name in self.matches:
dbg('Terminal::update_matches: refusing to add \
duplicate match %s' % name)
continue
self.matches[name] = self.vte.match_add(match)
dbg('Terminal::update_matches: added plugin URL handler \
for %s (%s)' % (name, urlplugin.__class__.__name__))
except Exception, ex:
err('Terminal::update_url_matches: %s' % ex)
def 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
self.matches[name] = self.vte.match_add(match)
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 connect_signals(self):
"""Connect all the gtk signals and drag-n-drop mechanics"""
self.vte.connect('key-press-event', self.on_keypress)
self.vte.connect('button-press-event', self.on_buttonpress)
self.vte.connect('popup-menu', self.popup_menu)
srcvtetargets = [("vte", gtk.TARGET_SAME_APP, self.TARGET_TYPE_VTE)]
dsttargets = [("vte", gtk.TARGET_SAME_APP, self.TARGET_TYPE_VTE),
('text/plain', 0, 0), ('STRING', 0, 0), ('COMPOUND_TEXT', 0, 0)]
for (widget, mask) in [
(self.vte, gtk.gdk.CONTROL_MASK | gtk.gdk.BUTTON3_MASK),
(self.titlebar, gtk.gdk.BUTTON1_MASK)]:
widget.drag_source_set(mask, srcvtetargets, gtk.gdk.ACTION_MOVE)
self.vte.drag_dest_set(gtk.DEST_DEFAULT_MOTION |
gtk.DEST_DEFAULT_HIGHLIGHT | gtk.DEST_DEFAULT_DROP,
dsttargets, gtk.gdk.ACTION_MOVE)
for widget in [self.vte, self.titlebar]:
widget.connect('drag-begin', self.on_drag_begin, self)
widget.connect('drag-data-get', self.on_drag_data_get,
self)
self.vte.connect('drag-motion', self.on_drag_motion, self)
self.vte.connect('drag-data-received',
self.on_drag_data_received, self)
# FIXME: Shouldn't this be in configure()?
if self.config['copy_on_selection']:
self.cnxids.new(self.vte, 'selection-changed',
lambda widget: self.vte.copy_clipboard())
if self.composite_support:
self.vte.connect('composited-changed', self.reconfigure)
self.vte.connect('window-title-changed', lambda x:
self.emit('title-change', self.get_window_title()))
self.vte.connect('grab-focus', self.on_vte_focus)
self.vte.connect('focus-in-event', self.on_vte_focus_in)
self.vte.connect('size-allocate', self.deferred_on_vte_size_allocate)
self.vte.add_events(gtk.gdk.ENTER_NOTIFY_MASK)
self.vte.connect('enter_notify_event',
self.on_vte_notify_enter)
self.cnxids.new(self.vte, 'realize', self.reconfigure)
def create_popup_group_menu(self, widget, event = None):
"""Pop up a menu for the group widget"""
if event:
button = event.button
time = event.time
else:
button = 0
time = 0
menu = self.populate_group_menu()
menu.show_all()
menu.popup(None, None, self.position_popup_group_menu, button, time,
widget)
return(True)
def populate_group_menu(self):
"""Fill out a group menu"""
menu = gtk.Menu()
groupitem = None
item = gtk.MenuItem(_('New group...'))
item.connect('activate', self.create_group)
menu.append(item)
if len(self.terminator.groups) > 0:
groupitem = gtk.RadioMenuItem(groupitem, _('None'))
groupitem.set_active(self.group == None)
groupitem.connect('activate', self.set_group, None)
menu.append(groupitem)
for group in self.terminator.groups:
item = gtk.RadioMenuItem(groupitem, group, False)
item.set_active(self.group == group)
item.connect('toggled', self.set_group, group)
menu.append(item)
groupitem = item
if self.group != None or len(self.terminator.groups) > 0:
menu.append(gtk.MenuItem())
if self.group != None:
item = gtk.MenuItem(_('Remove group %s') % self.group)
item.connect('activate', self.ungroup, self.group)
menu.append(item)
if util.has_ancestor(self, gtk.Notebook):
item = gtk.MenuItem(_('G_roup all in tab'))
item.connect('activate', lambda x: self.emit('group_tab'))
menu.append(item)
if len(self.terminator.groups) > 0:
item = gtk.MenuItem(_('Ungr_oup all in tab'))
item.connect('activate', lambda x: self.emit('ungroup_tab'))
menu.append(item)
if len(self.terminator.groups) > 0:
item = gtk.MenuItem(_('Remove all groups'))
item.connect('activate', lambda x: self.emit('ungroup-all'))
menu.append(item)
if self.group != None:
menu.append(gtk.MenuItem())
item = gtk.MenuItem(_('Close group %s') % self.group)
item.connect('activate', lambda x:
self.terminator.closegroupedterms(self.group))
menu.append(item)
menu.append(gtk.MenuItem())
groupitem = None
for key, value in {_('Broadcast all'):'all',
_('Broadcast group'):'group',
_('Broadcast off'):'off'}.items():
groupitem = gtk.RadioMenuItem(groupitem, key)
dbg('Terminal::populate_group_menu: %s active: %s' %
(key, self.terminator.groupsend ==
self.terminator.groupsend_type[value]))
groupitem.set_active(self.terminator.groupsend ==
self.terminator.groupsend_type[value])
groupitem.connect('activate', self.set_groupsend,
self.terminator.groupsend_type[value])
menu.append(groupitem)
menu.append(gtk.MenuItem())
item = gtk.CheckMenuItem(_('Split to this group'))
item.set_active(self.config['split_to_group'])
item.connect('toggled', lambda x: self.do_splittogroup_toggle())
menu.append(item)
item = gtk.CheckMenuItem(_('Autoclean groups'))
item.set_active(self.config['autoclean_groups'])
item.connect('toggled', lambda x: self.do_autocleangroups_toggle())
menu.append(item)
menu.append(gtk.MenuItem())
item = gtk.MenuItem(_('Insert terminal number'))
item.connect('activate', lambda x: self.emit('enumerate', False))
menu.append(item)
item = gtk.MenuItem(_('Insert padded terminal number'))
item.connect('activate', lambda x: self.emit('enumerate', True))
menu.append(item)
return(menu)
def position_popup_group_menu(self, menu, widget):
"""Calculate the position of the group popup menu"""
_screen_w = gtk.gdk.screen_width()
screen_h = gtk.gdk.screen_height()
if gtk.gtk_version >= (2, 14):
widget_win = widget.get_window()
else:
widget_win = widget.window
widget_x, widget_y = widget_win.get_origin()
_widget_w, widget_h = widget_win.get_size()
_menu_w, menu_h = menu.size_request()
if widget_y + widget_h + menu_h > screen_h:
menu_y = max(widget_y - menu_h, 0)
else:
menu_y = widget_y + widget_h
return(widget_x, menu_y, 1)
def set_group(self, _item, name):
"""Set a particular group"""
if self.group == name:
# already in this group, no action needed
return
dbg('Terminal::set_group: Setting group to %s' % name)
self.group = name
self.titlebar.set_group_label(name)
self.terminator.group_hoover()
def create_group(self, _item):
"""Trigger the creation of a group via the titlebar (because popup
windows are really lame)"""
self.titlebar.create_group()
def really_create_group(self, _widget, groupname):
"""The titlebar has spoken, let a group be created"""
self.terminator.create_group(groupname)
self.set_group(None, groupname)
def ungroup(self, _widget, data):
"""Remove a group"""
# FIXME: Could we emit and have Terminator do this?
for term in self.terminator.terminals:
if term.group == data:
term.set_group(None, None)
self.terminator.group_hoover()
def set_groupsend(self, _widget, value):
"""Set the groupsend mode"""
# FIXME: Can we think of a smarter way of doing this than poking?
if value in self.terminator.groupsend_type.values():
dbg('Terminal::set_groupsend: setting groupsend to %s' % value)
self.terminator.groupsend = value
def do_splittogroup_toggle(self):
"""Toggle the splittogroup mode"""
self.config['split_to_group'] = not self.config['split_to_group']
def do_autocleangroups_toggle(self):
"""Toggle the autocleangroups mode"""
self.config['autoclean_groups'] = not self.config['autoclean_groups']
def reconfigure(self, _widget=None):
"""Reconfigure our settings"""
dbg('Terminal::reconfigure')
self.cnxids.remove_signal(self.vte, 'realize')
# Handle child command exiting
self.cnxids.remove_signal(self.vte, 'child-exited')
if self.config['exit_action'] == 'restart':
self.cnxids.new(self.vte, 'child-exited', self.spawn_child, True)
elif self.config['exit_action'] in ('close', 'left'):
self.cnxids.new(self.vte, 'child-exited',
lambda x: self.emit('close-term'))
self.vte.set_emulation(self.config['emulation'])
if self.custom_encoding != True:
self.vte.set_encoding(self.config['encoding'])
self.vte.set_word_chars(self.config['word_chars'])
self.vte.set_mouse_autohide(self.config['mouse_autohide'])
backspace = self.config['backspace_binding']
delete = self.config['delete_binding']
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_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']:
fgcolor = self.vte.get_style().text[gtk.STATE_NORMAL]
bgcolor = self.vte.get_style().base[gtk.STATE_NORMAL]
else:
fgcolor = gtk.gdk.color_parse(self.config['foreground_color'])
bgcolor = gtk.gdk.color_parse(self.config['background_color'])
colors = self.config['palette'].split(':')
palette = []
for color in colors:
if color:
palette.append(gtk.gdk.color_parse(color))
self.vte.set_colors(fgcolor, bgcolor, palette)
if self.config['cursor_color'] != '':
self.vte.set_color_cursor(gtk.gdk.color_parse(
self.config['cursor_color']))
if hasattr(self.vte, 'set_cursor_shape'):
self.vte.set_cursor_shape(getattr(vte, 'CURSOR_SHAPE_' +
self.config['cursor_shape'].upper()))
background_type = self.config['background_type']
dbg('background_type=%s' % background_type)
if background_type == 'image' and \
self.config['background_image'] is not None and \
self.config['background_image'] != '':
self.vte.set_background_image_file(self.config['background_image'])
self.vte.set_scroll_background(self.config['scroll_background'])
else:
try:
self.vte.set_background_image(None)
except TypeError:
# FIXME: I think this is only necessary because of
# https://bugzilla.gnome.org/show_bug.cgi?id=614910
pass
self.vte.set_scroll_background(False)
if background_type in ('image', 'transparent'):
self.vte.set_background_tint_color(gtk.gdk.color_parse(
self.config['background_color']))
opacity = int(self.config['background_darkness'] * 65536)
saturation = 1.0 - float(self.config['background_darkness'])
dbg('setting background saturation: %f' % saturation)
self.vte.set_background_saturation(saturation)
else:
dbg('setting background_saturation: 1')
opacity = 65535
self.vte.set_background_saturation(1)
if self.composite_support:
dbg('setting opacity: %d' % opacity)
self.vte.set_opacity(opacity)
# This is quite hairy, but the basic explanation is that we should
# set_background_transparent(True) when we have no compositing and want
# fake background transparency, otherwise it should be False.
if not self.composite_support:
# We have no compositing support, fake background only
background_transparent = True
else:
if self.vte.is_composited() == False:
# We have compositing and it's enabled. no fake background.
background_transparent = True
else:
# We have compositing, but it's not enabled. fake background
background_transparent = False
if self.config['background_type'] == 'transparent':
dbg('setting background_transparent=%s' % background_transparent)
self.vte.set_background_transparent(background_transparent)
else:
dbg('setting background_transparent=False')
self.vte.set_background_transparent(False)
self.vte.set_cursor_blinks(self.config['cursor_blink'])
if self.config['force_no_bell'] == True:
self.vte.set_audible_bell(False)
self.vte.set_visible_bell(False)
self.cnxids.remove_signal(self.vte, 'beep')
else:
self.vte.set_audible_bell(self.config['audible_bell'])
self.vte.set_visible_bell(self.config['visible_bell'])
self.cnxids.remove_signal(self.vte, 'beep')
if self.config['urgent_bell'] == True or \
self.config['icon_bell'] == True:
try:
self.cnxids.new(self.vte, 'beep', self.on_beep)
except TypeError:
err('beep 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.scrollbar_position != self.config['scrollbar_position']:
self.scrollbar_position = self.config['scrollbar_position']
if self.config['scrollbar_position'] in ['disabled', 'hidden']:
self.scrollbar.hide()
else:
self.scrollbar.show()
if self.config['scrollbar_position'] == 'left':
self.reorder_child(self.scrollbar, 0)
elif self.config['scrollbar_position'] == 'right':
self.reorder_child(self.vte, 0)
if hasattr(self.vte, 'set_alternate_screen_scroll'):
self.vte.set_alternate_screen_scroll(
self.config['alternate_screen_scroll'])
self.titlebar.update()
self.vte.queue_draw()
def get_window_title(self):
"""Return the window title"""
return(self.vte.get_window_title() or str(self.command))
def on_group_button_press(self, widget, event):
"""Handler for the group button"""
if event.button == 1:
self.create_popup_group_menu(widget, event)
return(False)
def on_keypress(self, widget, event):
"""Handler for keyboard events"""
if not event:
dbg('Terminal::on_keypress: Called on %s with no event' % widget)
return(False)
# FIXME: Does keybindings really want to live in Terminator()?
mapping = self.terminator.keybindings.lookup(event)
if mapping == "hide_window":
return(False)
if mapping and mapping not in ['close_window',
'full_screen',
'new_tab']:
dbg('Terminal::on_keypress: lookup found: %r' % mapping)
# handle the case where user has re-bound copy to ctrl+<key>
# we only copy if there is a selection otherwise let it fall through
# to ^<key>
if (mapping == "copy" and event.state & gtk.gdk.CONTROL_MASK):
if self.vte.get_has_selection ():
getattr(self, "key_" + mapping)()
return(True)
else:
getattr(self, "key_" + mapping)()
return(True)
# FIXME: This is all clearly wrong. We should be doing this better
# maybe we can emit the key event and let Terminator() care?
groupsend = self.terminator.groupsend
groupsend_type = self.terminator.groupsend_type
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 event.button == 1:
# Ctrl+leftclick on a URL should open it
if event.state & gtk.gdk.CONTROL_MASK == gtk.gdk.CONTROL_MASK:
url = self.check_for_url(event)
if url:
self.open_url(url, prepare=True)
elif event.button == 2:
# middleclick should paste the clipboard
self.paste_clipboard(True)
return(True)
elif event.button == 3:
# rightclick should display a context menu if Ctrl is not pressed
if event.state & gtk.gdk.CONTROL_MASK == 0:
self.popup_menu(widget, event)
return(True)
return(False)
def popup_menu(self, widget, event=None):
"""Display the context menu"""
menu = TerminalPopupMenu(self)
menu.show(widget, event)
def do_scrollbar_toggle(self):
"""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"""
widget.drag_source_set_icon_pixbuf(util.widget_pixbuf(self, 512))
def on_drag_data_get(self, _widget, _drag_context, selection_data, info,
_time, data):
"""I have no idea what this does, drag and drop is a mystery. sorry."""
selection_data.set('vte', info,
str(data.terminator.terminals.index(self)))
def on_drag_motion(self, widget, drag_context, x, y, _time, _data):
"""*shrug*"""
if 'text/plain' in drag_context.targets:
# copy text from another widget
return
srcwidget = drag_context.get_source_widget()
if(isinstance(srcwidget, gtk.EventBox) and
srcwidget == self.titlebar) or widget == srcwidget:
# on self
return
alloc = widget.allocation
rect = gtk.gdk.Rectangle(0, 0, alloc.width, alloc.height)
if self.config['use_theme_colors']:
color = self.vte.get_style().text[gtk.STATE_NORMAL]
else:
color = gtk.gdk.color_parse(self.config['foreground_color'])
pos = self.get_location(widget, x, y)
topleft = (0, 0)
topright = (alloc.width, 0)
topmiddle = (alloc.width/2, 0)
bottomleft = (0, alloc.height)
bottomright = (alloc.width, alloc.height)
bottommiddle = (alloc.width/2, alloc.height)
middleleft = (0, alloc.height/2)
middleright = (alloc.width, alloc.height/2)
#print "%f %f %d %d" %(coef1, coef2, b1,b2)
coord = ()
if pos == "right":
coord = (topright, topmiddle, bottommiddle, bottomright)
elif pos == "top":
coord = (topleft, topright, middleright , middleleft)
elif pos == "left":
coord = (topleft, topmiddle, bottommiddle, bottomleft)
elif pos == "bottom":
coord = (bottomleft, bottomright, middleright , middleleft)
#here, we define some widget internal values
widget._expose_data = { 'color': color, 'coord' : coord }
#redraw by forcing an event
connec = widget.connect_after('expose-event', self.on_expose_event)
widget.window.invalidate_rect(rect, True)
widget.window.process_updates(True)
#finaly reset the values
widget.disconnect(connec)
widget._expose_data = None
def on_expose_event(self, widget, _event):
"""Handle an expose event while dragging"""
if not widget._expose_data:
return(False)
color = widget._expose_data['color']
coord = widget._expose_data['coord']
context = widget.window.cairo_create()
context.set_source_rgba(color.red, color.green, color.blue, 0.5)
if len(coord) > 0 :
context.move_to(coord[len(coord)-1][0], coord[len(coord)-1][1])
for i in coord:
context.line_to(i[0], i[1])
context.fill()
return(False)
def on_drag_data_received(self, widget, drag_context, x, y, selection_data,
_info, _time, data):
"""Something has been dragged into the terminal. Handle it as either a
URL or another terminal."""
if selection_data.type == 'text/plain':
# copy text to destination
txt = selection_data.data.strip()
if txt[0:7] == 'file://':
txt = "'%s'" % urllib.unquote(txt[7:])
for term in self.terminator.get_target_terms(self):
term.feed(txt)
return
widgetsrc = data.terminator.terminals[int(selection_data.data)]
srcvte = drag_context.get_source_widget()
#check if computation requireds
if (isinstance(srcvte, gtk.EventBox) and
srcvte == self.titlebar) or srcvte == widget:
return
srchbox = widgetsrc
# 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
coef1 = float(term.allocation.height)/float(term.allocation.width)
coef2 = -float(term.allocation.height)/float(term.allocation.width)
b1 = 0
b2 = term.allocation.height
#determine position in rectangle
#--------
#|\ /|
#| \ / |
#| \/ |
#| /\ |
#| / \ |
#|/ \|
#--------
if (x*coef1 + b1 > y ) and (x*coef2 + b2 < y ):
pos = "right"
if (x*coef1 + b1 > y ) and (x*coef2 + b2 > y ):
pos = "top"
if (x*coef1 + b1 < y ) and (x*coef2 + b2 > y ):
pos = "left"
if (x*coef1 + b1 < y ) and (x*coef2 + b2 < y ):
pos = "bottom"
return pos
def grab_focus(self):
"""Steal focus for this terminal"""
if not self.vte.flags()&gtk.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_child()
maker = Factory()
if maker.isinstance(topchild, 'Notebook'):
prevtmp = None
tmp = self.get_parent()
while tmp != topchild:
prevtmp = tmp
tmp = tmp.get_parent()
page = topchild.page_num(prevtmp)
topchild.set_current_page(page)
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.emit('focus-in')
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.vte.window and 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()
data['old_char_height'] = self.vte.get_char_height()
data['old_char_width'] = self.vte.get_char_width()
data['old_allocation'] = self.vte.get_allocation()
data['old_padding'] = self.vte.get_padding()
data['old_columns'] = self.vte.get_column_count()
data['old_rows'] = self.vte.get_row_count()
data['old_parent'] = self.get_parent()
return(data)
def zoom_scale(self, widget, allocation, old_data):
"""Scale our font correctly based on how big we are not vs before"""
self.cnxids.remove_signal(self, '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()
new_allocation = self.vte.get_allocation()
old_alloc = {'x': old_data['old_allocation'].width - \
old_data['old_padding'][0],
'y': old_data['old_allocation'].height - \
old_data['old_padding'][1]
}
dbg('Terminal::zoom_scale: Resized from %dx%d to %dx%d' % (
old_data['old_columns'],
old_data['old_rows'],
new_columns,
new_rows))
if new_rows == old_data['old_rows'] or \
new_columns == old_data['old_columns']:
dbg('Terminal::zoom_scale: One axis unchanged, not scaling')
return
old_area = old_data['old_columns'] * old_data['old_rows']
new_area = new_columns * new_rows
area_factor = (new_area / old_area) / 2
new_size = int(old_data['old_font'].get_size() * area_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
parent = self.get_parent()
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):
update_records = self.config['update_records']
login = self.config['login_shell']
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])
if 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
dbg('Forking shell: "%s" with args: %s' % (shell, args))
self.pid = self.vte.fork_command(command=shell, argv=args, envv=[],
loglastlog=login, logwtmp=update_records,
logutmp=update_records, directory=self.cwd)
self.command = shell
self.titlebar.update()
if self.pid == -1:
self.vte.feed(_('Unable to start shell:') + shell)
return(-1)
def check_for_url(self, event):
"""Check if the mouse is over a URL"""
return (self.vte.match_check(int(event.x / self.vte.get_char_width()),
int(event.y / self.vte.get_char_height())))
def prepare_url(self, urlmatch):
"""Prepare a URL from a VTE match"""
url = urlmatch[0]
match = urlmatch[1]
if match == self.matches['email'] and url[0:7] != 'mailto:':
url = 'mailto:' + url
elif match == self.matches['addr_only'] and url[0:3] == 'ftp':
url = 'ftp://' + url
elif match == self.matches['addr_only']:
url = 'http://' + url
elif match in self.matches.values():
# We have a match, but it's not a hard coded one, so it's a plugin
try:
registry = plugin.PluginRegistry()
registry.load_plugins()
plugins = registry.get_plugins_by_capability('url_handler')
for urlplugin in plugins:
if match == self.matches[urlplugin.handler_name]:
newurl = urlplugin.callback(url)
if newurl is not None:
dbg('Terminal::prepare_url: URL prepared by \
%s plugin' % urlplugin.handler_name)
url = newurl
break
except Exception, ex:
err('Terminal::prepare_url: %s' % ex)
return(url)
def open_url(self, url, prepare=False):
"""Open a given URL, conditionally unpacking it from a VTE match"""
oldstyle = False
if prepare == True:
url = self.prepare_url(url)
dbg('open_url: URL: %s (prepared: %s)' % (url, prepare))
if gtk.gtk_version < (2, 14, 0) or \
not hasattr(gtk, 'show_uri') or \
not hasattr(gtk.gdk, 'CURRENT_TIME'):
oldstyle = True
if oldstyle == False:
try:
gtk.show_uri(None, url, gtk.gdk.CURRENT_TIME)
except:
oldstyle = True
if oldstyle == True:
dbg('Old gtk (%s,%s,%s), calling xdg-open' % gtk.gtk_version)
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)
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_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"""
antialias = self.config['antialias']
if antialias:
antialias = vte.ANTI_ALIAS_FORCE_ENABLE
else:
antialias = vte.ANTI_ALIAS_FORCE_DISABLE
self.vte.set_font_full(fontdesc, antialias)
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_beep(self, widget):
"""Set the urgency hint for our window"""
if self.config['urgent_bell'] == True:
window = self.get_toplevel()
window.set_urgency_hint(True)
if self.config['icon_bell'] == True:
self.titlebar.icon_bell()
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
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'])
# There now begins a great list of keyboard event handlers
def key_zoom_in(self):
self.zoom_in()
def key_zoom_out(self):
self.zoom_out()
def key_copy(self):
self.vte.copy_clipboard()
def key_paste(self):
self.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.terminator.pid_cwd(self.pid))
def key_split_vert(self):
self.emit('split-vert', self.terminator.pid_cwd(self.pid))
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_ungroup_all(self):
self.emit('ungroup-all')
def key_group_tab(self):
self.emit('group-tab')
def key_ungroup_tab(self):
self.emit('ungroup-tab')
def key_new_window(self):
self.terminator.new_window(self.terminator.pid_cwd(self.pid))
def key_new_terminator(self):
cmd = sys.argv[0]
if not os.path.isabs(cmd):
# Command is not an absolute path. Figure out where we are
cmd = os.path.join (self.origcwd, sys.argv[0])
if not os.path.isfile(cmd):
# we weren't started as ./terminator in a path. Give up
err('Terminal::key_new_window: Unable to locate Terminator')
return False
dbg("Terminal::key_new_window: Spawning: %s" % cmd)
subprocess.Popen([cmd, '-u'])
def key_broadcast_off(self):
self.set_groupsend(None, self.terminator.groupsend_type['off'])
def key_broadcast_group(self):
self.set_groupsend(None, self.terminator.groupsend_type['group'])
def key_broadcast_all(self):
self.set_groupsend(None, self.terminator.groupsend_type['all'])
# End key events
gobject.type_register(Terminal)
# vim: set expandtab ts=4 sw=4: