1070 lines
36 KiB
Python
Executable File
1070 lines
36 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"""
|
|
|
|
import sys
|
|
import os
|
|
import pygtk
|
|
pygtk.require('2.0')
|
|
import gtk
|
|
import gobject
|
|
import pango
|
|
import re
|
|
import subprocess
|
|
|
|
from util import dbg, err, gerr, widget_pixbuf, get_top_window
|
|
import util
|
|
from config import Config
|
|
from cwd import get_default_cwd
|
|
from newterminator import Terminator
|
|
from titlebar import Titlebar
|
|
from terminal_popup_menu import TerminalPopupMenu
|
|
from searchbar import Searchbar
|
|
from translation import _
|
|
|
|
try:
|
|
import vte
|
|
except ImportError:
|
|
gerr('You need to install python bindings for libvte')
|
|
sys.exit(1)
|
|
|
|
# pylint: disable-msg=R0904
|
|
class Terminal(gtk.VBox):
|
|
"""Class implementing the VTE widget and its wrappings"""
|
|
|
|
__gsignals__ = {
|
|
'close-term': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
|
|
'title-change': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
|
|
(gobject.TYPE_STRING,)),
|
|
'enumerate': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
|
|
(gobject.TYPE_INT,)),
|
|
'group-tab': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
|
|
'ungroup-tab': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
|
|
'ungroup-all': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
|
|
'split-horiz': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
|
|
'split-vert': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
|
|
'tab-new': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
|
|
'tab-top-new': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
|
|
'focus-in': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
|
|
'zoom': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
|
|
'maximise': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
|
|
'unzoom': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
|
|
'resize-term': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
|
|
(gobject.TYPE_STRING,)),
|
|
'navigate': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
|
|
(gobject.TYPE_STRING,)),
|
|
}
|
|
|
|
TARGET_TYPE_VTE = 8
|
|
|
|
terminator = None
|
|
vte = None
|
|
terminalbox = None
|
|
scrollbar = None
|
|
titlebar = None
|
|
searchbar = None
|
|
|
|
group = None
|
|
cwd = None
|
|
command = None
|
|
clipboard = None
|
|
pid = None
|
|
|
|
matches = None
|
|
config = None
|
|
default_encoding = None
|
|
|
|
composite_support = None
|
|
|
|
confcnxid = None
|
|
zoomcnxid = None
|
|
|
|
def __init__(self):
|
|
"""Class initialiser"""
|
|
gtk.VBox.__init__(self)
|
|
self.__gobject_init__()
|
|
|
|
self.terminator = Terminator()
|
|
self.connect('enumerate', self.terminator.do_enumerate)
|
|
self.connect('group-tab', self.terminator.group_tab)
|
|
self.connect('ungroup-tab', self.terminator.ungroup_tab)
|
|
self.connect('focus-in', self.terminator.focus_changed)
|
|
|
|
self.matches = {}
|
|
|
|
self.config = Config()
|
|
|
|
self.cwd = get_default_cwd()
|
|
self.clipboard = gtk.clipboard_get(gtk.gdk.SELECTION_CLIPBOARD)
|
|
|
|
self.vte = vte.Terminal()
|
|
self.vte.set_size(80, 24)
|
|
self.vte._expose_data = None
|
|
if not hasattr(self.vte, "set_opacity") or \
|
|
not hasattr(self.vte, "is_composited"):
|
|
self.composite_support = False
|
|
self.vte.show()
|
|
|
|
self.default_encoding = self.vte.get_encoding()
|
|
self.update_url_matches(self.config['try_posix_regexp'])
|
|
|
|
self.terminalbox = self.create_terminalbox()
|
|
|
|
self.titlebar = Titlebar()
|
|
self.titlebar.connect_icon(self.on_group_button_press)
|
|
self.titlebar.connect('edit-done', self.on_edit_done)
|
|
self.connect('title-change', self.titlebar.set_terminal_title)
|
|
self.titlebar.connect('create-group', self.really_create_group)
|
|
|
|
self.searchbar = Searchbar()
|
|
self.searchbar.connect('end-search', self.on_search_done)
|
|
|
|
self.show()
|
|
self.pack_start(self.titlebar, False)
|
|
self.pack_start(self.terminalbox)
|
|
self.pack_end(self.searchbar)
|
|
|
|
self.connect_signals()
|
|
|
|
os.putenv('COLORTERM', 'gnome-terminal')
|
|
|
|
env_proxy = os.getenv('http_proxy')
|
|
if not env_proxy:
|
|
if self.config['http_proxy'] and self.config['http_proxy'] != '':
|
|
os.putenv('http_proxy', self.config['http_proxy'])
|
|
|
|
def close(self):
|
|
"""Close ourselves"""
|
|
self.emit('close-term')
|
|
|
|
def create_terminalbox(self):
|
|
"""Create a GtkHBox containing the terminal and a scrollbar"""
|
|
|
|
terminalbox = gtk.HBox()
|
|
self.scrollbar = gtk.VScrollbar(self.vte.get_adjustment())
|
|
position = self.config['scrollbar_position']
|
|
|
|
if position not in ('hidden', 'disabled'):
|
|
self.scrollbar.show()
|
|
|
|
if position == 'left':
|
|
func = terminalbox.pack_end
|
|
else:
|
|
func = terminalbox.pack_start
|
|
|
|
func(self.vte)
|
|
func(self.scrollbar, False)
|
|
terminalbox.show()
|
|
|
|
return(terminalbox)
|
|
|
|
def update_url_matches(self, posix = True):
|
|
"""Update the regexps used to match URLs"""
|
|
userchars = "-A-Za-z0-9"
|
|
passchars = "-A-Za-z0-9,?;.:/!%$^*&~\"#'"
|
|
hostchars = "-A-Za-z0-9"
|
|
pathchars = "-A-Za-z0-9_$.+!*(),;:@&=?/~#%'\""
|
|
schemes = "(news:|telnet:|nntp:|file:/|https?:|ftps?:|webcal:)"
|
|
user = "[" + userchars + "]+(:[" + passchars + "]+)?"
|
|
urlpath = "/[" + pathchars + "]*[^]'.}>) \t\r\n,\\\"]"
|
|
|
|
if posix:
|
|
dbg ('Terminal::update_url_matches: Trying POSIX URL regexps')
|
|
lboundry = "[[:<:]]"
|
|
rboundry = "[[:>:]]"
|
|
else: # GNU
|
|
dbg ('Terminal::update_url_matches: Trying GNU URL regexps')
|
|
lboundry = "\\<"
|
|
rboundry = "\\>"
|
|
|
|
self.matches['full_uri'] = self.vte.match_add(lboundry + schemes +
|
|
"//(" + user + "@)?[" + hostchars +".]+(:[0-9]+)?(" +
|
|
urlpath + ")?" + rboundry + "/?")
|
|
|
|
if self.matches['full_uri'] == -1:
|
|
if posix:
|
|
err ('Terminal::update_url_matches: POSIX match failed, trying GNU')
|
|
self.update_url_matches(posix = False)
|
|
else:
|
|
err ('Terminal::update_url_matches: Failed adding URL match patterns')
|
|
else:
|
|
self.matches['voip'] = self.vte.match_add(lboundry +
|
|
'(callto:|h323:|sip:)' + "[" + userchars + "+][" +
|
|
userchars + ".]*(:[0-9]+)?@?[" + pathchars + "]+" +
|
|
rboundry)
|
|
self.matches['addr_only'] = self.vte.match_add (lboundry +
|
|
"(www|ftp)[" + hostchars + "]*\.[" + hostchars +
|
|
".]+(:[0-9]+)?(" + urlpath + ")?" + rboundry + "/?")
|
|
self.matches['email'] = self.vte.match_add (lboundry +
|
|
"(mailto:)?[a-zA-Z0-9][a-zA-Z0-9.+-]*@[a-zA-Z0-9]\
|
|
[a-zA-Z0-9-]*\.[a-zA-Z0-9][a-zA-Z0-9-]+\
|
|
[.a-zA-Z0-9-]*" + rboundry)
|
|
self.matches['nntp'] = self.vte.match_add (lboundry +
|
|
'''news:[-A-Z\^_a-z{|}~!"#$%&'()*+,./0-9;:=?`]+@\
|
|
[-A-Za-z0-9.]+(:[0-9]+)?''' + rboundry)
|
|
# if the url looks like a Launchpad changelog closure entry
|
|
# LP: #92953 - make it a url to http://bugs.launchpad.net
|
|
self.matches['launchpad'] = self.vte.match_add (
|
|
'\\bLP:? #?[0-9]+\\b')
|
|
|
|
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)
|
|
|
|
if self.config['copy_on_selection']:
|
|
self.vte.connect('selection-changed', lambda widget:
|
|
self.vte.copy_clipboard())
|
|
|
|
if self.composite_support:
|
|
self.vte.connect('composited-changed',
|
|
self.on_composited_changed)
|
|
|
|
self.vte.connect('window-title-changed', lambda x:
|
|
self.emit('title-change', self.get_window_title()))
|
|
self.vte.connect('grab-focus', self.on_vte_focus)
|
|
self.vte.connect('focus-in-event', self.on_vte_focus_in)
|
|
self.vte.connect('size-allocate', self.on_vte_size_allocate)
|
|
|
|
if self.config['exit_action'] == 'restart':
|
|
self.vte.connect('child-exited', self.spawn_child)
|
|
elif self.config['exit_action'] in ('close', 'left'):
|
|
self.vte.connect('child-exited', lambda x: self.emit('close-term'))
|
|
|
|
self.vte.add_events(gtk.gdk.ENTER_NOTIFY_MASK)
|
|
self.vte.connect('enter_notify_event',
|
|
self.on_vte_notify_enter)
|
|
|
|
self.confcnxid = self.vte.connect_after('realize', self.reconfigure)
|
|
|
|
def create_popup_group_menu(self, widget, event = None):
|
|
"""Pop up a menu for the group widget"""
|
|
if event:
|
|
button = event.button
|
|
time = event.time
|
|
else:
|
|
button = 0
|
|
time = 0
|
|
|
|
menu = self.populate_group_menu()
|
|
menu.show_all()
|
|
menu.popup(None, None, self.position_popup_group_menu, button, time,
|
|
widget)
|
|
return(True)
|
|
|
|
def populate_group_menu(self):
|
|
"""Fill out a group menu"""
|
|
menu = gtk.Menu()
|
|
groupitem = None
|
|
|
|
item = gtk.MenuItem(_('New group...'))
|
|
item.connect('activate', self.create_group)
|
|
menu.append(item)
|
|
|
|
if len(self.terminator.groups) > 0:
|
|
groupitem = gtk.RadioMenuItem(groupitem, _('None'))
|
|
groupitem.set_active(self.group == None)
|
|
groupitem.connect('activate', self.set_group, None)
|
|
menu.append(groupitem)
|
|
|
|
for group in self.terminator.groups:
|
|
item = gtk.RadioMenuItem(groupitem, group, False)
|
|
item.set_active(self.group == group)
|
|
item.connect('toggled', self.set_group, group)
|
|
menu.append(item)
|
|
groupitem = item
|
|
|
|
if self.group != None or len(self.terminator.groups) > 0:
|
|
menu.append(gtk.MenuItem())
|
|
|
|
if self.group != None:
|
|
item = gtk.MenuItem(_('Remove group %s') % self.group)
|
|
item.connect('activate', self.ungroup, self.group)
|
|
menu.append(item)
|
|
|
|
if util.has_ancestor(self, gtk.Notebook):
|
|
item = gtk.MenuItem(_('G_roup all in tab'))
|
|
item.connect('activate', lambda x: self.emit('group_tab'))
|
|
menu.append(item)
|
|
|
|
if len(self.terminator.groups) > 0:
|
|
item = gtk.MenuItem(_('Ungr_oup all in tab'))
|
|
item.connect('activate', lambda x: self.emit('ungroup_tab'))
|
|
menu.append(item)
|
|
|
|
if len(self.terminator.groups) > 0:
|
|
item = gtk.MenuItem(_('Remove all groups'))
|
|
item.connect('activate', lambda x: self.emit('ungroup-all'))
|
|
menu.append(item)
|
|
|
|
if self.group != None:
|
|
menu.append(gtk.MenuItem())
|
|
|
|
item = gtk.MenuItem(_('Close group %s') % self.group)
|
|
item.connect('activate', lambda x:
|
|
self.terminator.closegroupedterms(self.group))
|
|
menu.append(item)
|
|
|
|
menu.append(gtk.MenuItem())
|
|
|
|
groupitem = None
|
|
|
|
for key, value in {_('Broadcast all'):'all',
|
|
_('Broadcast group'):'group',
|
|
_('Broadcast off'):'off'}.items():
|
|
groupitem = gtk.RadioMenuItem(groupitem, key)
|
|
dbg('Terminal::populate_group_menu: %s active: %s' %
|
|
(key, self.terminator.groupsend ==
|
|
self.terminator.groupsend_type[value]))
|
|
groupitem.set_active(self.terminator.groupsend ==
|
|
self.terminator.groupsend_type[value])
|
|
groupitem.connect('activate', self.set_groupsend,
|
|
self.terminator.groupsend_type[value])
|
|
menu.append(groupitem)
|
|
|
|
menu.append(gtk.MenuItem())
|
|
|
|
item = gtk.CheckMenuItem(_('Split to this group'))
|
|
item.set_active(self.config['split_to_group'])
|
|
item.connect('toggled', lambda x: self.do_splittogroup_toggle())
|
|
menu.append(item)
|
|
|
|
item = gtk.CheckMenuItem(_('Autoclean groups'))
|
|
item.set_active(self.config['autoclean_groups'])
|
|
item.connect('toggled', lambda x: self.do_autocleangroups_toggle())
|
|
menu.append(item)
|
|
|
|
menu.append(gtk.MenuItem())
|
|
|
|
item = gtk.MenuItem(_('Insert terminal number'))
|
|
item.connect('activate', lambda x: self.emit('enumerate', False))
|
|
menu.append(item)
|
|
|
|
item = gtk.MenuItem(_('Insert padded terminal number'))
|
|
item.connect('activate', lambda x: self.emit('enumerate', True))
|
|
menu.append(item)
|
|
|
|
return(menu)
|
|
|
|
def position_popup_group_menu(self, menu, widget):
|
|
"""Calculate the position of the group popup menu"""
|
|
screen_w = gtk.gdk.screen_width()
|
|
screen_h = gtk.gdk.screen_height()
|
|
|
|
widget_win = widget.get_window()
|
|
widget_x, widget_y = widget_win.get_origin()
|
|
widget_w, widget_h = widget_win.get_size()
|
|
|
|
menu_w, menu_h = menu.size_request()
|
|
|
|
if widget_y + widget_h + menu_h > screen_h:
|
|
menu_y = max(widget_y - menu_h, 0)
|
|
else:
|
|
menu_y = widget_y + widget_h
|
|
|
|
return(widget_x, menu_y, 1)
|
|
|
|
def set_group(self, item, name):
|
|
"""Set a particular group"""
|
|
if self.group == name:
|
|
# already in this group, no action needed
|
|
return
|
|
dbg('Terminal::set_group: Setting group to %s' % name)
|
|
self.group = name
|
|
self.titlebar.set_group_label(name)
|
|
self.terminator.group_hoover()
|
|
|
|
def create_group(self, item):
|
|
"""Trigger the creation of a group via the titlebar (because popup
|
|
windows are really lame)"""
|
|
self.titlebar.create_group()
|
|
|
|
def really_create_group(self, widget, groupname):
|
|
"""The titlebar has spoken, let a group be created"""
|
|
self.terminator.create_group(groupname)
|
|
self.set_group(None, groupname)
|
|
|
|
def ungroup(self, widget, data):
|
|
"""Remove a group"""
|
|
# FIXME: Could we emit and have Terminator do this?
|
|
for term in self.terminator.terminals:
|
|
if term.group == data:
|
|
term.set_group(None, None)
|
|
self.terminator.group_hoover()
|
|
|
|
def set_groupsend(self, widget, value):
|
|
"""Set the groupsend mode"""
|
|
# FIXME: Can we think of a smarter way of doing this than poking?
|
|
if value in self.terminator.groupsend_type.values():
|
|
dbg('Terminal::set_groupsend: setting groupsend to %s' % value)
|
|
self.terminator.groupsend = value
|
|
|
|
def do_splittogroup_toggle(self):
|
|
"""Toggle the splittogroup mode"""
|
|
self.config['split_to_group'] = not self.config['split_to_group']
|
|
|
|
def do_autocleangroups_toggle(self):
|
|
"""Toggle the autocleangroups mode"""
|
|
self.config['autoclean_groups'] = not self.config['autoclean_groups']
|
|
|
|
def reconfigure(self, widget=None):
|
|
"""Reconfigure our settings"""
|
|
dbg('Terminal::reconfigure')
|
|
if self.confcnxid:
|
|
self.vte.disconnect(self.confcnxid)
|
|
self.confcnxid = None
|
|
|
|
# FIXME: actually reconfigure our settings
|
|
pass
|
|
|
|
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']:
|
|
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
|
|
# FIXMS: maybe we can emit the key event and let Terminator() care?
|
|
if self.terminator.groupsend != 0 and self.vte.is_focus():
|
|
if self.group and self.terminator.groupsend == 1:
|
|
self.terminator.group_emit(self, self.group, 'key-press-event',
|
|
event)
|
|
if self.terminator.groupsend == 2:
|
|
self.terminator.all_emit(self, 'key-press-event', event)
|
|
|
|
return(False)
|
|
|
|
def on_buttonpress(self, widget, event):
|
|
"""Handler for mouse events"""
|
|
# Any button event should grab focus
|
|
widget.grab_focus()
|
|
|
|
if event.button == 1:
|
|
# Ctrl+leftclick on a URL should open it
|
|
if event.state & gtk.gdk.CONTROL_MASK == gtk.gdk.CONTROL_MASK:
|
|
url = self.check_for_url(event)
|
|
if url:
|
|
self.open_url(url, prepare=True)
|
|
elif event.button == 2:
|
|
# middleclick should paste the clipboard
|
|
self.paste_clipboard(True)
|
|
return(True)
|
|
elif event.button == 3:
|
|
# rightclick should display a context menu if Ctrl is not pressed
|
|
if event.state & gtk.gdk.CONTROL_MASK == 0:
|
|
self.popup_menu(widget, event)
|
|
return(True)
|
|
|
|
return(False)
|
|
|
|
def popup_menu(self, widget, event=None):
|
|
"""Display the context menu"""
|
|
menu = TerminalPopupMenu(self)
|
|
menu.show(widget, event)
|
|
|
|
def do_scrollbar_toggle(self):
|
|
self.toggle_widget_visibility(self.scrollbar)
|
|
|
|
def do_title_toggle(self):
|
|
self.toggle_widget_visibility(self.titlebar)
|
|
|
|
def toggle_widget_visibility(self, widget):
|
|
if widget.get_property('visible'):
|
|
widget.hide()
|
|
else:
|
|
widget.show()
|
|
|
|
def on_encoding_change(self, widget, encoding):
|
|
"""Handle the encoding changing"""
|
|
current = self.vte.get_encoding()
|
|
if current != encoding:
|
|
dbg('on_encoding_change: setting encoding to: %s' % encoding)
|
|
self.custom_encoding = not (encoding == self.config['encoding'])
|
|
self.vte.set_encoding(encoding)
|
|
|
|
def on_drag_begin(self, widget, drag_context, data):
|
|
"""Handle the start of a drag event"""
|
|
widget.drag_source_set_icon_pixbuf(util.widget_pixbuf(self, 512))
|
|
|
|
def on_drag_data_get(self, widget, drag_context, selection_data, info, time,
|
|
data):
|
|
"""I have no idea what this does, drag and drop is a mystery. sorry."""
|
|
selection_data.set('vte', info,
|
|
str(data.terminator.terminals.index(self)))
|
|
|
|
def on_drag_motion(self, widget, drag_context, x, y, time, data):
|
|
"""*shrug*"""
|
|
if 'text/plain' in drag_context.targets:
|
|
# copy text from another widget
|
|
return
|
|
srcwidget = drag_context.get_source_widget()
|
|
if(isinstance(srcwidget, gtk.EventBox) and
|
|
srcwidget == self.titlebar) or widget == srcwidget:
|
|
# on self
|
|
return
|
|
|
|
alloc = widget.allocation
|
|
rect = gtk.gdk.Rectangle(0, 0, alloc.width, alloc.height)
|
|
|
|
if self.config['use_theme_colors']:
|
|
color = self.vte.get_style().text[gtk.STATE_NORMAL]
|
|
else:
|
|
color = gtk.gdk.color_parse(self.config['foreground_color'])
|
|
|
|
pos = self.get_location(widget, x, y)
|
|
topleft = (0,0)
|
|
topright = (alloc.width,0)
|
|
topmiddle = (alloc.width/2,0)
|
|
bottomleft = (0, alloc.height)
|
|
bottomright = (alloc.width,alloc.height)
|
|
bottommiddle = (alloc.width/2, alloc.height)
|
|
middle = (alloc.width/2, alloc.height/2)
|
|
middleleft = (0, alloc.height/2)
|
|
middleright = (alloc.width, alloc.height/2)
|
|
#print "%f %f %d %d" %(coef1, coef2, b1,b2)
|
|
coord = ()
|
|
if pos == "right":
|
|
coord = (topright, topmiddle, bottommiddle, bottomright)
|
|
elif pos == "top":
|
|
coord = (topleft, topright, middleright , middleleft)
|
|
elif pos == "left":
|
|
coord = (topleft, topmiddle, bottommiddle, bottomleft)
|
|
elif pos == "bottom":
|
|
coord = (bottomleft, bottomright, middleright , middleleft)
|
|
|
|
#here, we define some widget internal values
|
|
widget._expose_data = { 'color': color, 'coord' : coord }
|
|
#redraw by forcing an event
|
|
connec = widget.connect_after('expose-event', self.on_expose_event)
|
|
widget.window.invalidate_rect(rect, True)
|
|
widget.window.process_updates(True)
|
|
#finaly reset the values
|
|
widget.disconnect(connec)
|
|
widget._expose_data = None
|
|
|
|
def on_expose_event(self, widget, event):
|
|
"""Handle an expose event while dragging"""
|
|
if not widget._expose_data:
|
|
return(False)
|
|
|
|
color = widget._expose_data['color']
|
|
coord = widget._expose_data['coord']
|
|
|
|
context = widget.window.cairo_create()
|
|
context.set_source_rgba(color.red, color.green, color.blue, 0.5)
|
|
if len(coord) > 0 :
|
|
context.move_to(coord[len(coord)-1][0],coord[len(coord)-1][1])
|
|
for i in coord:
|
|
context.line_to(i[0],i[1])
|
|
|
|
context.fill()
|
|
return(False)
|
|
|
|
def on_drag_data_received(self, widget, drag_context, x, y, selection_data,
|
|
info, time, data):
|
|
if selection_data.type == 'text/plain':
|
|
# copy text to destination
|
|
txt = selection_data.data.strip()
|
|
if txt[0:7] == 'file://':
|
|
text = "'%s'" % urllib.unquote(txt[7:])
|
|
for term in self.terminator.get_target_terms():
|
|
term.feed(txt)
|
|
return
|
|
|
|
widgetsrc = data.terminator.terminals[int(selection_data.data)]
|
|
srcvte = drag_context.get_source_widget()
|
|
#check if computation requireds
|
|
if (isinstance(srcvte, gtk.EventBox) and
|
|
srcvte == self.titlebar) or srcvte == widget:
|
|
return
|
|
|
|
srchbox = widgetsrc
|
|
dsthbox = widget.get_parent().get_parent()
|
|
|
|
dstpaned = dsthbox.get_parent()
|
|
srcpaned = srchbox.get_parent()
|
|
if isinstance(dstpaned, gtk.Window) and isinstance(srcpaned, gtk.Window):
|
|
return
|
|
|
|
pos = self.get_location(widget, x, y)
|
|
|
|
srcpaned.remove(widgetsrc)
|
|
dstpaned.split_axis(dsthbox, pos in ['top', 'bottom'], widgetsrc)
|
|
srcpaned.hoover()
|
|
|
|
def get_location(self, vte, x, y):
|
|
"""Get our location within the terminal"""
|
|
pos = ''
|
|
#get the diagonales function for the receiving widget
|
|
coef1 = float(vte.allocation.height)/float(vte.allocation.width)
|
|
coef2 = -float(vte.allocation.height)/float(vte.allocation.width)
|
|
b1 = 0
|
|
b2 = vte.allocation.height
|
|
#determine position in rectangle
|
|
"""
|
|
--------
|
|
|\ /|
|
|
| \ / |
|
|
| \/ |
|
|
| /\ |
|
|
| / \ |
|
|
|/ \|
|
|
--------
|
|
"""
|
|
if (x*coef1 + b1 > y ) and (x*coef2 + b2 < y ):
|
|
pos = "right"
|
|
if (x*coef1 + b1 > y ) and (x*coef2 + b2 > y ):
|
|
pos = "top"
|
|
if (x*coef1 + b1 < y ) and (x*coef2 + b2 > y ):
|
|
pos = "left"
|
|
if (x*coef1 + b1 < y ) and (x*coef2 + b2 < y ):
|
|
pos = "bottom"
|
|
return pos
|
|
|
|
def grab_focus(self):
|
|
self.vte.grab_focus()
|
|
|
|
def on_vte_focus(self, widget):
|
|
self.emit('title-change', self.get_window_title())
|
|
|
|
def on_vte_focus_out(self, widget, event):
|
|
return
|
|
|
|
def on_vte_focus_in(self, widget, event):
|
|
self.emit('focus-in')
|
|
|
|
def scrollbar_jump(self, position):
|
|
"""Move the scrollbar to a particular row"""
|
|
self.scrollbar.set_value(position)
|
|
|
|
def scrollbar_position(self):
|
|
"""Return the current position of the scrollbar"""
|
|
return(self.scrollbar.get_value())
|
|
|
|
def on_search_done(self, widget):
|
|
"""We've finished searching, so clean up"""
|
|
self.searchbar.hide()
|
|
self.scrollbar.set_value(self.vte.get_cursor_position()[1])
|
|
self.vte.grab_focus()
|
|
|
|
def on_edit_done(self, widget):
|
|
"""A child widget is done editing a label, return focus to VTE"""
|
|
self.vte.grab_focus()
|
|
|
|
def on_vte_size_allocate(self, widget, allocation):
|
|
self.titlebar.update_terminal_size(self.vte.get_column_count(),
|
|
self.vte.get_row_count())
|
|
|
|
def on_vte_notify_enter(self, term, event):
|
|
"""Handle the mouse entering this terminal"""
|
|
if self.config['focus'] in ['sloppy', 'mouse']:
|
|
term.grab_focus()
|
|
return(False)
|
|
|
|
def hide_titlebar(self):
|
|
"""Hide the titlebar"""
|
|
self.titlebar.hide()
|
|
|
|
def show_titlebar(self):
|
|
"""Show the titlebar"""
|
|
self.titlebar.show()
|
|
|
|
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.disconnect(self.zoomcnxid)
|
|
self.zoomcnxid = None
|
|
|
|
new_columns = self.vte.get_column_count()
|
|
new_rows = self.vte.get_row_count()
|
|
new_font = self.vte.get_font()
|
|
new_allocation = self.vte.get_allocation()
|
|
|
|
old_alloc = {'x': old_data['old_allocation'].width - \
|
|
old_data['old_padding'][0],
|
|
'y': old_data['old_allocation'].height - \
|
|
old_data['old_padding'][1]
|
|
}
|
|
|
|
dbg('Terminal::zoom_scale: Resized from %dx%d to %dx%d' % (
|
|
old_data['old_columns'],
|
|
old_data['old_rows'],
|
|
new_columns,
|
|
new_rows))
|
|
|
|
if (new_rows == old_data['old_rows'] or new_columns == old_data['old_columns']):
|
|
dbg('Terminal::zoom_scale: One axis unchanged, not scaling')
|
|
return
|
|
|
|
old_area = old_data['old_columns'] * old_data['old_rows']
|
|
new_area = new_columns * new_rows
|
|
area_factor = (new_area / old_area) / 2
|
|
|
|
new_font.set_size(old_data['old_font'].get_size() * area_factor)
|
|
self.vte.set_font(new_font)
|
|
|
|
def is_zoomed(self):
|
|
"""Determine if we are a zoomed terminal"""
|
|
prop = None
|
|
parent = self.get_parent()
|
|
window = get_top_window(self)
|
|
|
|
try:
|
|
prop = window.get_property('term-zoomed')
|
|
except TypeError:
|
|
prop = False
|
|
|
|
return(prop)
|
|
|
|
def zoom(self, widget=None):
|
|
"""Zoom ourself to fill the window"""
|
|
self.emit('zoom')
|
|
|
|
def maximise(self, widget=None):
|
|
"""Maximise ourself to fill the window"""
|
|
self.emit('maximise')
|
|
|
|
def unzoom(self, widget=None):
|
|
"""Restore normal layout"""
|
|
self.emit('unzoom')
|
|
|
|
def spawn_child(self, widget=None):
|
|
update_records = self.config['update_records']
|
|
login = self.config['login_shell']
|
|
args = []
|
|
shell = None
|
|
command = None
|
|
|
|
self.vte.grab_focus()
|
|
|
|
if self.config['use_custom_command']:
|
|
command = self.config['custom_command']
|
|
|
|
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
|
|
|
|
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 == self.matches['launchpad']:
|
|
for item in re.findall(r'[0-9]+', url):
|
|
url = 'https://bugs.launchpad.net/bugs/%s' % item
|
|
return(url)
|
|
else:
|
|
return(url)
|
|
|
|
def open_url(self, url, prepare=False):
|
|
"""Open a given URL, conditionally unpacking it from a VTE match"""
|
|
if prepare == True:
|
|
url = self.prepare_url(url)
|
|
dbg('open_url: URL: %s (prepared: %s)' % (url, prepare))
|
|
gtk.show_uri(None, url, gtk.gdk.CURRENT_TIME)
|
|
|
|
def paste_clipboard(self, primary=False):
|
|
"""Paste one of the two clipboards"""
|
|
for term in self.terminator.get_target_terms(self):
|
|
if primary:
|
|
term.vte.paste_primary()
|
|
else:
|
|
term.vte.paste_clipboard()
|
|
self.vte.grab_focus()
|
|
|
|
def feed(self, text):
|
|
"""Feed the supplied text to VTE"""
|
|
self.vte.feed_child(text)
|
|
|
|
def zoom_in(self):
|
|
"""Increase the font size"""
|
|
self.zoom_font(True)
|
|
|
|
def zoom_out(self):
|
|
"""Decrease the font size"""
|
|
self.zoom_font(False)
|
|
|
|
def zoom_font(self, zoom_in):
|
|
"""Change the font size"""
|
|
pangodesc = self.vte.get_font()
|
|
fontsize = pangodesc.get_size()
|
|
|
|
if fontsize > pango.SCALE and not zoom_in:
|
|
fontsize -= pango.SCALE
|
|
elif zoom_in:
|
|
fontsize += pango.SCALE
|
|
|
|
pangodesc.set_size(fontsize)
|
|
self.vte.set_font(pangodesc)
|
|
|
|
def zoom_orig(self):
|
|
"""Restore original font size"""
|
|
dbg("Terminal::zoom_orig: restoring font to: %s" % self.config['font'])
|
|
self.vte.set_font(pango.FontDescription(self.config['font']))
|
|
|
|
# There now begins a great list of keyboard event handlers
|
|
# FIXME: Probably a bunch of these are wrong. TEST!
|
|
def key_zoom_in(self):
|
|
self.zoom_in()
|
|
|
|
def key_zoom_out(self):
|
|
self.zoom_out()
|
|
|
|
def key_copy(self):
|
|
self.vte.copy_clipboard()
|
|
|
|
def key_paste(self):
|
|
self.vte.paste_clipboard()
|
|
|
|
def key_toggle_scrollbar(self):
|
|
self.do_scrollbar_toggle()
|
|
|
|
def key_zoom_normal(self):
|
|
self.zoom_orig ()
|
|
|
|
def key_search(self):
|
|
self.searchbar.start_search()
|
|
|
|
# bindings that should be moved to Terminator as they all just call
|
|
# a function of Terminator. It would be cleaner if TerminatorTerm
|
|
# has absolutely no reference to Terminator.
|
|
# N (next) - P (previous) - O (horizontal) - E (vertical) - W (close)
|
|
def key_new_root_tab(self):
|
|
self.terminator.newtab (self, True)
|
|
|
|
def key_go_next(self):
|
|
self.emit('navigate', 'next')
|
|
|
|
def key_go_prev(self):
|
|
self.emit('navigate', 'prev')
|
|
|
|
def key_go_up(self):
|
|
self.emit('navigate', 'up')
|
|
|
|
def key_go_down(self):
|
|
self.emit('navigate', 'down')
|
|
|
|
def key_go_left(self):
|
|
self.emit('navigate', 'left')
|
|
|
|
def key_go_right(self):
|
|
self.emit('navigate', 'right')
|
|
|
|
def key_split_horiz(self):
|
|
self.emit('split-horiz')
|
|
|
|
def key_split_vert(self):
|
|
self.emit('split-vert')
|
|
|
|
def key_close_term(self):
|
|
self.terminator.closeterm (self)
|
|
|
|
def key_new_tab(self):
|
|
self.terminator.newtab(self)
|
|
|
|
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.terminator.move_tab (self, 'right')
|
|
|
|
def key_move_tab_left(self):
|
|
self.terminator.move_tab (self, '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.terminator.next_tab (self)
|
|
|
|
def key_prev_tab(self):
|
|
self.terminator.previous_tab (self)
|
|
|
|
def key_switch_to_tab_1(self):
|
|
self.terminator.switch_to_tab (self, 0)
|
|
|
|
def key_switch_to_tab_2(self):
|
|
self.terminator.switch_to_tab (self, 1)
|
|
|
|
def key_switch_to_tab_3(self):
|
|
self.terminator.switch_to_tab (self, 2)
|
|
|
|
def key_switch_to_tab_4(self):
|
|
self.terminator.switch_to_tab (self, 3)
|
|
|
|
def key_switch_to_tab_5(self):
|
|
self.terminator.switch_to_tab (self, 4)
|
|
|
|
def key_switch_to_tab_6(self):
|
|
self.terminator.switch_to_tab (self, 5)
|
|
|
|
def key_switch_to_tab_7(self):
|
|
self.terminator.switch_to_tab (self, 6)
|
|
|
|
def key_switch_to_tab_8(self):
|
|
self.terminator.switch_to_tab (self, 7)
|
|
|
|
def key_switch_to_tab_9(self):
|
|
self.terminator.switch_to_tab (self, 8)
|
|
|
|
def key_switch_to_tab_10(self):
|
|
self.terminator.switch_to_tab (self, 9)
|
|
|
|
def key_reset(self):
|
|
self.vte.reset (True, False)
|
|
|
|
def key_reset_clear(self):
|
|
self.vte.reset (True, True)
|
|
|
|
def key_group_all(self):
|
|
self.group_all(self)
|
|
|
|
def key_ungroup_all(self):
|
|
self.ungroup_all(self)
|
|
|
|
def key_group_tab(self):
|
|
self.group_tab(self)
|
|
|
|
def key_ungroup_tab(self):
|
|
self.ungroup_tab(self)
|
|
|
|
def key_new_window(self):
|
|
cmd = sys.argv[0]
|
|
|
|
if not os.path.isabs(cmd):
|
|
# Command is not an absolute path. Figure out where we are
|
|
cmd = os.path.join (self.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,])
|
|
# End key events
|
|
|
|
gobject.type_register(Terminal)
|
|
# vim: set expandtab ts=4 sw=4:
|