453 lines
16 KiB
Python
Executable File
453 lines
16 KiB
Python
Executable File
#!/usr/bin/python
|
|
# Terminator - multiple gnome terminals in one window
|
|
# Copyright (C) 2006-2007 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
|
|
|
|
import sys
|
|
import string
|
|
import gobject
|
|
import gtk
|
|
import vte
|
|
import gconf
|
|
import pango
|
|
import gnome
|
|
import time
|
|
|
|
class TerminatorTerm:
|
|
lastreconfigure = 0
|
|
|
|
# Our settings
|
|
# FIXME: Add commandline and/or gconf options to change these
|
|
defaults = {
|
|
'profile_dir' : '/apps/gnome-terminal/profiles/',
|
|
'profile' : 'Default',
|
|
'allow_bold' : True,
|
|
'audible_bell' : False,
|
|
'background' : None,
|
|
'background_color' : '#000000',
|
|
'backspace_binding' : 'ascii-del',
|
|
'delete_binding' : 'delete-sequence',
|
|
'cursor_blinks' : False,
|
|
'emulation' : 'xterm',
|
|
'font_name' : 'Serif 10',
|
|
'foreground_color' : '#AAAAAA',
|
|
'scrollbar' : True,
|
|
'scroll_on_keystroke' : False,
|
|
'scroll_on_output' : False,
|
|
'scrollback_lines' : 100,
|
|
'focus' : 'sloppy',
|
|
'visible_bell' : False,
|
|
'child_restart' : True,
|
|
'link_scheme' : '(news|telnet|nttp|file|http|ftp|https)',
|
|
'_link_user' : '[%s]+(:[%s]+)?',
|
|
'link_hostchars' : '-A-Za-z0-9',
|
|
'link_userchars' : '-A-Za-z0-9',
|
|
'link_passchars' : '-A-Za-z0-9,?;.:/!%$^*&~"#\'',
|
|
'palette' : '/apps/gnome-terminal/profiles/Default/palette',
|
|
}
|
|
|
|
def __init__ (self, term, settings = {}):
|
|
self.defaults['link_user'] = self.defaults['_link_user']%(self.defaults['link_userchars'], self.defaults['link_passchars'])
|
|
|
|
# Set up any overridden settings
|
|
for key in settings.keys ():
|
|
defaults[key] = settings[key]
|
|
|
|
self.term = term
|
|
self.profile = self.defaults['profile_dir'] + self.defaults['profile']
|
|
|
|
self.gconf_client = gconf.client_get_default ()
|
|
self.gconf_client.add_dir (self.profile, gconf.CLIENT_PRELOAD_RECURSIVE)
|
|
self.gconf_client.add_dir ('/apps/metacity/general', gconf.CLIENT_PRELOAD_RECURSIVE)
|
|
|
|
self.clipboard = gtk.clipboard_get (gtk.gdk.SELECTION_CLIPBOARD)
|
|
|
|
self._vte = vte.Terminal ()
|
|
self._vte.set_size (5, 5)
|
|
self.reconfigure_vte ()
|
|
self._vte.show ()
|
|
|
|
self._box = gtk.HBox ()
|
|
self._scrollbar = gtk.VScrollbar (self._vte.get_adjustment ())
|
|
if self.defaults['scrollbar']:
|
|
self._scrollbar.show ()
|
|
|
|
self._box.pack_start (self._vte)
|
|
self._box.pack_start (self._scrollbar, False)
|
|
|
|
self.gconf_client.notify_add (self.profile, self.on_gconf_notification)
|
|
self.gconf_client.notify_add ('/apps/metacity/general/focus_mode', self.on_gconf_notification)
|
|
|
|
self._vte.connect ("button-press-event", self.on_vte_button_press)
|
|
self._vte.connect ("popup-menu", self.on_vte_popup_menu)
|
|
|
|
if self.gconf_client.get_string (self.profile + "/exit_action") == "restart":
|
|
self._vte.connect ("child-exited", self.spawn_child)
|
|
|
|
self._vte.add_events (gtk.gdk.ENTER_NOTIFY_MASK)
|
|
self._vte.connect ("enter_notify_event", self.on_vte_notify_enter)
|
|
|
|
self._vte.match_add ('((%s://(%s@)?)|(www|ftp)[%s]*\\.)[%s.]+(:[0-9]*)?'%(self.defaults['link_scheme'], self.defaults['link_user'], self.defaults['link_hostchars'], self.defaults['link_hostchars']))
|
|
self._vte.match_add ('((%s://(%s@)?)|(www|ftp)[%s]*\\.)[%s.]+(:[0-9]+)?/[-A-Za-z0-9_$.+!*(),;:@&=?/~#%%]*[^]\'.}>) \t\r\n,\\\]'%(self.defaults['link_scheme'], self.defaults['link_userchars'], self.defaults['link_hostchars'], self.defaults['link_hostchars']))
|
|
|
|
self.spawn_child ()
|
|
|
|
def spawn_child (self, event=None):
|
|
if self.gconf_client.get_bool (self.profile + "/use_custom_command") == True:
|
|
self._vte.fork_command (self.gconf_client.get_string (self.profile + "/custom_command"))
|
|
else:
|
|
self._vte.fork_command ()
|
|
|
|
def reconfigure_vte (self):
|
|
if ((self.lastreconfigure != 0) and (time.time () - self.lastreconfigure) < 5):
|
|
# Rate limit
|
|
return
|
|
self.lastreconfigure = time.time ()
|
|
|
|
# Set our emulation
|
|
self._vte.set_emulation (self.defaults['emulation'])
|
|
|
|
# Set our wordchars
|
|
# FIXME: This shouldn't be hardcoded
|
|
self._vte.set_word_chars ('-A-Za-z0-9./?%&#_+')
|
|
|
|
# Set our mouselation
|
|
# FIXME: This shouldn't be hardcoded
|
|
self._vte.set_mouse_autohide (True)
|
|
|
|
# Set our compatibility
|
|
backspace = self.gconf_client.get_string (self.profile + "/backspace_binding") or self.defaults['backspace_binding']
|
|
delete = self.gconf_client.get_string (self.profile + "/delete_binding") or self.defaults['delete_binding']
|
|
|
|
# Note, each of the 4 following comments should replace the line beneath it, but the python-vte bindings don't appear to support this constant, so the magic values are being assumed from the C enum :/
|
|
if backspace == "ascii-del":
|
|
# backbind = vte.ERASE_ASCII_BACKSPACE
|
|
backbind = 2
|
|
else:
|
|
# backbind = vte.ERASE_AUTO_BACKSPACE
|
|
backbind = 1
|
|
|
|
if delete == "escape-sequence":
|
|
# delbind = vte.ERASE_DELETE_SEQUENCE
|
|
delbind = 3
|
|
else:
|
|
# delbind = vte.ERASE_AUTO
|
|
delbind = 0
|
|
|
|
self._vte.set_backspace_binding (backbind)
|
|
self._vte.set_delete_binding (delbind)
|
|
|
|
# Set our font, preferably from gconf settings
|
|
if self.gconf_client.get_bool (self.profile + "/use_system_font"):
|
|
font_name = (self.gconf_client.get_string ("/desktop/gnome/interface/monospace_font_name") or self.defaults['font_name'])
|
|
else:
|
|
font_name = (self.gconf_client.get_string (self.profile + "/font") or self.defaults['font_name'])
|
|
|
|
try:
|
|
self._vte.set_font (pango.FontDescription (font_name))
|
|
except:
|
|
pass
|
|
|
|
# Set our boldness
|
|
self._vte.set_allow_bold (self.gconf_client.get_bool (self.profile + "/allow_bold") or self.defaults['allow_bold'])
|
|
|
|
# Set our color scheme, preferably from gconf settings
|
|
palette = self.gconf_client.get_string (self.profile + "/palette") or self.defaults['palette']
|
|
if self.gconf_client.get_bool (self.profile + "/use_theme_colors") == True:
|
|
# FIXME: For some reason this isn't working properly, but the code appears to be analogous to what gnome-terminal does in C
|
|
fg_color = self._vte.get_style().text[gtk.STATE_NORMAL]
|
|
bg_color = self._vte.get_style().base[gtk.STATE_NORMAL]
|
|
else:
|
|
fg_color = gtk.gdk.color_parse (self.gconf_client.get_string (self.profile + "/foreground_color") or self.defaults['foreground_color'])
|
|
bg_color = gtk.gdk.color_parse (self.gconf_client.get_string (self.profile + "/background_color") or self.defaults['background_color'])
|
|
|
|
colors = palette.split (':')
|
|
palette = []
|
|
for color in colors:
|
|
palette.append (gtk.gdk.color_parse (color))
|
|
self._vte.set_colors (fg_color, bg_color, palette)
|
|
|
|
# Set our cursor blinkiness
|
|
self._vte.set_cursor_blinks = (self.gconf_client.get_bool (self.profile + "/cursor_blinks") or self.defaults['cursor_blinks'])
|
|
|
|
# Set our audible belliness
|
|
self._vte.set_audible_bell = not (self.gconf_client.get_bool (self.profile + "/silent_bell") or self.defaults['audible_bell'])
|
|
self._vte.set_visible_bell (self.defaults['visible_bell'])
|
|
|
|
# Set our scrolliness
|
|
self._vte.set_scrollback_lines (self.gconf_client.get_int (self.profile + "/scrollback_lines") or self.defaults['scrollback_lines'])
|
|
self._vte.set_scroll_on_keystroke (self.gconf_client.get_bool (self.profile + "/scroll_on_keystroke") or self.defaults['scroll_on_keystroke'])
|
|
self._vte.set_scroll_on_output (self.gconf_client.get_bool (self.profile + "/scroll_on_output") or self.defaults['scroll_on_output'])
|
|
|
|
# Set our sloppiness
|
|
self.focus = self.gconf_client.get_string ("/apps/metacity/general/focus_mode") or self.defaults['focus']
|
|
|
|
def on_gconf_notification (self, client, cnxn_id, entry, what):
|
|
self.reconfigure_vte ()
|
|
|
|
def on_vte_button_press (self, term, event):
|
|
# Left mouse button should transfer focus to this vte widget
|
|
if event.button == 1:
|
|
self._vte.grab_focus ()
|
|
return False
|
|
|
|
# Right mouse button should display a context menu
|
|
if event.button == 3:
|
|
self.do_popup (event)
|
|
return True
|
|
|
|
def on_vte_notify_enter (self, term, event):
|
|
if (self.focus == "sloppy" or self.focus == "mouse"):
|
|
term.grab_focus ()
|
|
# FIXME: Should we eat this event or let it propagate further?
|
|
return False
|
|
|
|
def do_scrollbar_toggle (self):
|
|
if self._scrollbar.get_property ('visible'):
|
|
self._scrollbar.hide ()
|
|
else:
|
|
self._scrollbar.show ()
|
|
|
|
def on_vte_popup_menu (self, term):
|
|
self.do_popup ()
|
|
|
|
def do_popup (self, event = None):
|
|
menu = self.create_popup_menu (event)
|
|
menu.popup (None, None, None, event.button, event.time)
|
|
|
|
def create_popup_menu (self, event):
|
|
menu = gtk.Menu ()
|
|
|
|
url = self._vte.match_check (int(event.x / self._vte.get_char_width ()), int(event.y / self._vte.get_char_height()))
|
|
if url:
|
|
item = gtk.MenuItem ("_Open Link")
|
|
item.connect ("activate", lambda menu_item: gnome.url_show (url[0]))
|
|
menu.append (item)
|
|
|
|
item = gtk.MenuItem ("_Copy Link Address")
|
|
item.connect ("activate", lambda menu_item: self.clipboard.set_text (url[0]))
|
|
menu.append (item)
|
|
|
|
item = gtk.MenuItem ()
|
|
menu.append (item)
|
|
|
|
item = gtk.ImageMenuItem (gtk.STOCK_COPY)
|
|
item.connect ("activate", lambda menu_item: self._vte.copy_clipboard ())
|
|
item.set_sensitive (self._vte.get_has_selection ())
|
|
menu.append (item)
|
|
|
|
item = gtk.ImageMenuItem (gtk.STOCK_PASTE)
|
|
item.connect ("activate", lambda menu_item: self._vte.paste_clipboard ())
|
|
menu.append (item)
|
|
|
|
item = gtk.MenuItem ()
|
|
menu.append (item)
|
|
|
|
item = gtk.CheckMenuItem ("Show scrollbar")
|
|
item.set_active (self._scrollbar.get_property ('visible'))
|
|
item.connect ("toggled", lambda menu_item: self.do_scrollbar_toggle ())
|
|
menu.append (item)
|
|
|
|
item = gtk.MenuItem ()
|
|
menu.append (item)
|
|
|
|
item = gtk.MenuItem ("Split _Horizontally")
|
|
item.connect ("activate", lambda menu_item: self.term.splithoriz (self))
|
|
menu.append (item)
|
|
|
|
item = gtk.MenuItem ("Split _Vertically")
|
|
item.connect ("activate", lambda menu_item: self.term.splitvert (self))
|
|
menu.append (item)
|
|
|
|
item = gtk.MenuItem ()
|
|
menu.append (item)
|
|
|
|
item = gtk.ImageMenuItem (gtk.STOCK_CLOSE)
|
|
item.connect ("activate", lambda menu_item: self.term.closeterm (self))
|
|
menu.append (item)
|
|
|
|
menu.show_all ()
|
|
return menu
|
|
|
|
def get_box (self):
|
|
return self._box
|
|
|
|
class Terminator:
|
|
def __init__ (self):
|
|
self.gconf_client = gconf.client_get_default ()
|
|
|
|
self.window = gtk.Window ()
|
|
self.icon = self.window.render_icon (gtk.STOCK_DIALOG_INFO, gtk.ICON_SIZE_BUTTON)
|
|
self.window.set_icon (self.icon)
|
|
self.window.connect ("delete_event", self.on_delete_event)
|
|
self.window.connect ("destroy", self.on_destroy_event)
|
|
self.window.maximize ()
|
|
|
|
# Start out with just one terminal
|
|
# FIXME: This should be really be decided from some kind of profile
|
|
term = (TerminatorTerm (self))
|
|
|
|
self.window.add (term.get_box ())
|
|
self.window.show_all ()
|
|
|
|
gobject.timeout_add (1000, self.do_initial_setup, term)
|
|
|
|
def do_initial_setup (self, term):
|
|
term2 = self.splitvert (term)
|
|
self.splithoriz (term)
|
|
self.splithoriz (term2)
|
|
return (False)
|
|
|
|
def on_delete_event (self, widget, event, data=None):
|
|
dialog = gtk.Dialog ("Quit?", self.window, gtk.DIALOG_MODAL, (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, gtk.STOCK_QUIT, gtk.RESPONSE_ACCEPT))
|
|
label = gtk.Label("Do you really want to quit?")
|
|
dialog.vbox.pack_start(label, True, True, 0)
|
|
label.show ()
|
|
|
|
res = dialog.run()
|
|
if res == gtk.RESPONSE_ACCEPT:
|
|
return False
|
|
dialog.destroy ()
|
|
return True
|
|
|
|
def on_destroy_event (self, widget, data=None):
|
|
gtk.main_quit ()
|
|
|
|
def splithoriz (self, widget):
|
|
term2 = TerminatorTerm (self)
|
|
|
|
parent = widget.get_box ().get_parent()
|
|
pane = gtk.HPaned ()
|
|
cols = widget._vte.get_column_count ()
|
|
rows = widget._vte.get_row_count ()
|
|
allowance = widget._scrollbar.allocation.width + pane.style_get_property ('handle-size')
|
|
widget._vte.set_size ((cols / 2) - (allowance / widget._vte.get_char_width ()), rows)
|
|
term2._vte.set_size ((cols / 2) - (allowance / widget._vte.get_char_width ()), rows)
|
|
|
|
if isinstance (parent, gtk.Window):
|
|
# We just have one term
|
|
termwidth = parent.allocation.width / 2
|
|
widget.get_box ().reparent (pane)
|
|
|
|
pane.add1 (widget.get_box ())
|
|
pane.add2 (term2.get_box ())
|
|
|
|
parent.add (pane)
|
|
pane.set_position (termwidth)
|
|
|
|
if isinstance (parent, gtk.Paned):
|
|
# We are inside a split term
|
|
if (widget.get_box () == parent.get_child1 ()):
|
|
widget.get_box ().reparent (pane)
|
|
parent.add1 (pane)
|
|
else:
|
|
widget.get_box ().reparent (pane)
|
|
parent.add2 (pane)
|
|
|
|
pane.add1 (widget.get_box ())
|
|
pane.add2 (term2.get_box ())
|
|
|
|
parent.show_all ()
|
|
return (term2)
|
|
|
|
def splitvert (self, widget):
|
|
term2 = TerminatorTerm (self)
|
|
|
|
parent = widget.get_box ().get_parent()
|
|
pane = gtk.VPaned ()
|
|
cols = widget._vte.get_column_count ()
|
|
rows = widget._vte.get_row_count ()
|
|
allowance = widget._scrollbar.allocation.width + pane.style_get_property ('handle-size')
|
|
widget._vte.set_size (cols, (rows / 2) - (allowance / widget._vte.get_char_height ()))
|
|
term2._vte.set_size (cols, (rows / 2) - (allowance / widget._vte.get_char_height ()))
|
|
|
|
if isinstance (parent, gtk.Window):
|
|
# We just have one term
|
|
termheight = parent.allocation.height / 2
|
|
widget.get_box ().reparent (pane)
|
|
|
|
pane.add1 (widget.get_box ())
|
|
pane.add2 (term2.get_box ())
|
|
|
|
parent.add (pane)
|
|
pane.set_position (termheight)
|
|
|
|
if isinstance (parent, gtk.Paned):
|
|
# We are inside a split term
|
|
term2._vte.set_size (cols, (rows / 2) - 1)
|
|
|
|
if (widget.get_box () == parent.get_child1 ()):
|
|
widget.get_box ().reparent (pane)
|
|
parent.add1 (pane)
|
|
else:
|
|
widget.get_box ().reparent (pane)
|
|
parent.add2 (pane)
|
|
|
|
pane.add1 (widget.get_box ())
|
|
pane.add2 (term2.get_box ())
|
|
|
|
parent.show_all ()
|
|
return (term2)
|
|
|
|
def closeterm (self, widget):
|
|
parent = widget.get_box ().get_parent ()
|
|
sibling = None
|
|
|
|
if isinstance (parent, gtk.Window):
|
|
# We are the only term
|
|
if not self.on_delete_event (parent, gtk.gdk.Event (gtk.gdk.DELETE)):
|
|
self.on_destroy_event (parent, gtk.gdk.Event (gtk.gdk.DESTROY))
|
|
return
|
|
if isinstance (parent, gtk.Paned):
|
|
grandparent = parent.get_parent ()
|
|
|
|
# Discover sibling while all objects exist
|
|
if widget.get_box () == parent.get_child1 ():
|
|
sibling = parent.get_child2 ()
|
|
if widget.get_box () == parent.get_child2 ():
|
|
sibling = parent.get_child1 ()
|
|
|
|
if not sibling:
|
|
# something is wrong, give up
|
|
print "Error: %s is not a child of %s"%(widget, parent)
|
|
return
|
|
|
|
if not self.closetermreq ():
|
|
grandparent.remove (parent)
|
|
sibling.reparent (grandparent)
|
|
widget.get_box ().destroy ()
|
|
parent.destroy ()
|
|
|
|
self.window.show_all ()
|
|
|
|
return
|
|
|
|
def closetermreq (self):
|
|
dialog = gtk.Dialog ("Close?", self.window, gtk.DIALOG_MODAL, (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, gtk.STOCK_CLOSE, gtk.RESPONSE_ACCEPT))
|
|
label = gtk.Label("Do you really want to close the active terminal?")
|
|
dialog.vbox.pack_start(label, True, True, 0)
|
|
label.show ()
|
|
|
|
res = dialog.run()
|
|
dialog.destroy ()
|
|
if res == gtk.RESPONSE_ACCEPT:
|
|
return False
|
|
return True
|
|
|
|
if __name__ == '__main__':
|
|
term = Terminator ()
|
|
gtk.main ()
|
|
|