#!/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 try: import vte except: print '''You need to install python bindings for libvte ("python-vte" in debian/ubuntu)''' sys.exit (1) 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 () # For some unknown reason, spawning 4 terminals at the speed python executes on my laptop causes them all to hang, so we will make one, wait a second and then split ourselves, which works fine. Weird. 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 splitaxis (self, vert=True, widget): term2 = TerminatorTerm (self) parent = widget.get_box ().get_parent () if vert: pane = gtk.VPaned () else: pane = gtk.HPaned () # VTE doesn't seem to cope well with being resized by the window manager. I expect I am supposed to send some kind of WINCH, or just generally connect window resizing events to a callback that will often tell vte about the new size. For now, cheat. Badly. cols = widget._vte.get_column_count () rows = widget._vte.get_row_count () allowance = widget._scrollbar.allocation.width + pane.style_get_property ('handle-size') if vert: width = cols height = (rows / 2) - (allowance / widget._vte.get_char_height ()) else: width = (cols / 2) - (allowance / widget._vte.get_char_width ()) height = rows widget._vte.set_size (width, height) term2._vte.set_size (width, height) if isinstance (parent, gtk.Window): # We have just one term if vert: newpos = parent.allocation.height / 2 else: newpos = parent.allocation.width / 2 widget.get_box ().reparent (pane) pane.add1 (widget.get_box ()) pane.add2 (widget.get_box ()) if isinstance (parent, gtk.Paned): # We are inside a split term if vert: term2._vte.set_size (cols, (rows / 2) - 1) else: # TESTING: Is this necessary? term2._vte.set_size (rows, (cols / 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 splithoriz (self, widget): term2 = TerminatorTerm (self) parent = widget.get_box ().get_parent() pane = gtk.HPaned () # VTE doesn't seem to cope well with being resized by the window manager. I expect I am supposed to send some kind of WINCH, or just generally connect window resizing events to a callback that will often tell vte about the new size. For now, cheat. Badly. 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 this 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 ()