hotwire-ssh r2 - in trunk: . bin hotssh hotssh/hotlib hotssh/hotlib_ui hotssh/hotvte images



Author: walters
Date: Tue May 20 15:11:46 2008
New Revision: 2
URL: http://svn.gnome.org/viewvc/hotwire-ssh?rev=2&view=rev

Log:
Initial import from Hotwire.


Added:
   trunk/COPYING
   trunk/MAINTAINERS
   trunk/MANIFEST.in
   trunk/bin/
   trunk/bin/hotwire-ssh   (contents, props changed)
   trunk/hotssh/
   trunk/hotssh/__init__.py
   trunk/hotssh/hotlib/
   trunk/hotssh/hotlib/__init__.py
   trunk/hotssh/hotlib/logutil.py
   trunk/hotssh/hotlib/timesince.py
   trunk/hotssh/hotlib_ui/
   trunk/hotssh/hotlib_ui/__init__.py
   trunk/hotssh/hotlib_ui/msgarea.py
   trunk/hotssh/hotlib_ui/quickfind.py
   trunk/hotssh/hotvte/
   trunk/hotssh/hotvte/__init__.py
   trunk/hotssh/hotvte/vteterm.py
   trunk/hotssh/hotvte/vtewindow.py
   trunk/hotssh/sshwindow.py
   trunk/hotssh/version.py
   trunk/hotwire-ssh.csh
   trunk/hotwire-ssh.desktop
   trunk/hotwire-ssh.sh
   trunk/images/
   trunk/images/hotwire-openssh.png   (contents, props changed)
   trunk/setup.py

Added: trunk/COPYING
==============================================================================
--- (empty file)
+++ trunk/COPYING	Tue May 20 15:11:46 2008
@@ -0,0 +1,8 @@
+The Hotwire Shell project uses two licenses.
+
+The license for the API (parts of Hotwire for use by other programs) is
+under a simple X11-style permissive license.  See COPYING.API.
+
+The license for the user interface is avilable under the GNU General
+Public License.  See COPYING.UI.
+

Added: trunk/MAINTAINERS
==============================================================================
--- (empty file)
+++ trunk/MAINTAINERS	Tue May 20 15:11:46 2008
@@ -0,0 +1,3 @@
+Colin Walters
+E-mail: walters redhat com
+Userid: walters

Added: trunk/MANIFEST.in
==============================================================================
--- (empty file)
+++ trunk/MANIFEST.in	Tue May 20 15:11:46 2008
@@ -0,0 +1,6 @@
+include hotwire-ssh.desktop
+include COPYING
+include images/*.png
+include images/*.gif
+include images/*.ico
+include hotwire-ssh.*sh
\ No newline at end of file

Added: trunk/bin/hotwire-ssh
==============================================================================
--- (empty file)
+++ trunk/bin/hotwire-ssh	Tue May 20 15:11:46 2008
@@ -0,0 +1,71 @@
+#!/usr/bin/python
+# This file is part of the Hotwire Shell user interface.
+#   
+# Copyright (C) 2007 Colin Walters <walters verbum org>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+import sys, os
+
+# We want to avoid running under the controlling terminal if we were passed
+# --bg, which by default comes from our sh/csh alias 
+if len(sys.argv) > 1 and sys.argv[1] == '--bg' and os.isatty(0):
+    pid = os.fork()
+    if pid == 0:
+       del sys.argv[1]
+       os.setsid()
+    else:
+       sys.exit(0)
+
+if __name__ == '__main__' and hasattr(sys.modules['__main__'], '__file__'):
+    basedir = os.path.dirname(os.path.abspath(__file__))
+    up_basedir = os.path.dirname(basedir)
+    if os.path.exists(os.path.join(up_basedir, 'COPYING')):
+        print "Detected COPYING; Apparently running uninstalled, extending path"
+        sys.path.insert(0, up_basedir)
+        os.environ['HOTWIRE_UNINSTALLED'] = up_basedir
+        os.environ['PYTHONPATH'] = os.pathsep.join(sys.path)
+        
+exec_real_ssh = False
+try:
+	import gtk
+except:
+	# No gtk, clearly no hotwire-ssh
+	exec_real_ssh = True
+if not exec_real_ssh:
+	# These options are ones which are mostly designed to be used from
+	# an existing Unix terminal.
+	for arg in ['-n', '-s', '-T']:
+		if arg in sys.argv[1:]:
+			exec_real_ssh = True
+			break
+if not exec_real_ssh:
+	# If we have at least two nonoptions, then the command is of the form:
+	# user host command ...
+	# This style is designed to get output into an existing display generally
+	nonoption_count = 0
+	for arg in sys.argv[1:]:
+		if not arg.startswith('-'):
+			nonoption_count += 1
+	if nonoption_count > 1:
+		exec_real_ssh = True 
+
+if exec_real_ssh:
+	os.execvp('ssh', ['ssh'] + sys.argv[1:])
+
+from hotssh.sshwindow import SshApp
+from hotssh.hotvte.vtewindow import VteMain
+
+VteMain().main(SshApp)

Added: trunk/hotssh/__init__.py
==============================================================================
--- (empty file)
+++ trunk/hotssh/__init__.py	Tue May 20 15:11:46 2008
@@ -0,0 +1,18 @@
+# This file is part of the Hotwire Shell user interface.
+#   
+# Copyright (C) 2007 Colin Walters <walters verbum org>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+

Added: trunk/hotssh/hotlib/__init__.py
==============================================================================

Added: trunk/hotssh/hotlib/logutil.py
==============================================================================
--- (empty file)
+++ trunk/hotssh/hotlib/logutil.py	Tue May 20 15:11:46 2008
@@ -0,0 +1,47 @@
+# This file is part of the Hotwire Shell project API.
+
+# Copyright (C) 2007 Colin Walters <walters verbum org>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy 
+# of this software and associated documentation files (the "Software"), to deal 
+# in the Software without restriction, including without limitation the rights 
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 
+# of the Software, and to permit persons to whom the Software is furnished to do so, 
+# subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in all 
+# copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 
+# PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE 
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
+# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR 
+# THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import sys, logging, logging.config, StringIO
+
+def log_except(logger=None, text=''):
+    def annotate(func):
+        def _exec_cb(*args, **kwargs):
+            try:
+                return func(*args, **kwargs)
+            except:
+                log_target = logger or logging
+                log_target.exception('Exception in callback%s', text and (': '+text) or '')
+        return _exec_cb
+    return annotate
+
+def init(default_level, debug_modules, prefix=None):
+    rootlog = logging.getLogger() 
+    fmt = logging.Formatter("%(asctime)s [%(thread)d] %(name)s %(levelname)s %(message)s",
+                            "%H:%M:%S")
+    stderr_handler = logging.StreamHandler(sys.stderr)
+    stderr_handler.setFormatter(fmt)
+    
+    rootlog.setLevel(default_level)
+    rootlog.addHandler(stderr_handler)
+    for logger in [logging.getLogger(prefix+x) for x in debug_modules]:
+        logger.setLevel(logging.DEBUG)
+
+    logging.debug("Initialized logging")

Added: trunk/hotssh/hotlib/timesince.py
==============================================================================
--- (empty file)
+++ trunk/hotssh/hotlib/timesince.py	Tue May 20 15:11:46 2008
@@ -0,0 +1,87 @@
+# This is lifted from Django:
+# http://code.djangoproject.com/browser/django/trunk/django/utils/timesince.py
+#Copyright (c) 2005, the Lawrence Journal-World
+#All rights reserved.
+#
+#Redistribution and use in source and binary forms, with or without modification,
+#are permitted provided that the following conditions are met:
+#
+#    1. Redistributions of source code must retain the above copyright notice, 
+#       this list of conditions and the following disclaimer.
+#    
+#    2. Redistributions in binary form must reproduce the above copyright 
+#       notice, this list of conditions and the following disclaimer in the
+#       documentation and/or other materials provided with the distribution.
+#
+#    3. Neither the name of Django nor the names of its contributors may be used
+#       to endorse or promote products derived from this software without
+#       specific prior written permission.
+#
+#THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+#ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+#WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+#DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+#ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+#(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+#LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+#ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+#(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+#SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import datetime
+from gettext import ngettext
+
+def timesince(d, now=None):
+    """
+    Takes two datetime objects and returns the time between d and now
+    as a nicely formatted string, e.g. "10 minutes".  If d occurs after now,
+    then "0 minutes" is returned.
+
+    Units used are years, months, weeks, days, hours, and minutes.
+    Seconds and microseconds are ignored.  Up to two adjacent units will be
+    displayed.  For example, "2 weeks, 3 days" and "1 year, 3 months" are
+    possible outputs, but "2 weeks, 3 hours" and "1 year, 5 days" are not.
+
+    Adapted from http://blog.natbat.co.uk/archive/2003/Jun/14/time_since
+    """
+    chunks = (
+      (60 * 60 * 24 * 365, lambda n: ngettext('year', 'years', n)),
+      (60 * 60 * 24 * 30, lambda n: ngettext('month', 'months', n)),
+      (60 * 60 * 24 * 7, lambda n : ngettext('week', 'weeks', n)),
+      (60 * 60 * 24, lambda n : ngettext('day', 'days', n)),
+      (60 * 60, lambda n: ngettext('hour', 'hours', n)),
+      (60, lambda n: ngettext('minute', 'minutes', n))
+    )
+    # Convert datetime.date to datetime.datetime for comparison
+    if d.__class__ is not datetime.datetime:
+        d = datetime.datetime(d.year, d.month, d.day)
+    if now:
+        t = now.timetuple()
+    else:
+        t = time.localtime()
+    tz = None
+    now = datetime.datetime(t[0], t[1], t[2], t[3], t[4], t[5], tzinfo=tz)
+
+    # ignore microsecond part of 'd' since we removed it from 'now'
+    delta = now - (d - datetime.timedelta(0, 0, d.microsecond))
+    since = delta.days * 24 * 60 * 60 + delta.seconds
+    if since <= 0:
+        # d is in the future compared to now, stop processing.
+        return _('in the future')
+    if since < 60:
+        return _('less than 1 minute ago')    
+    for i, (seconds, name) in enumerate(chunks):
+        count = since // seconds
+        if count != 0:
+            break
+    s = None   
+    if i + 1 < len(chunks):
+        # Now get the second item
+        seconds2, name2 = chunks[i + 1]
+        count2 = (since - (seconds * count)) // seconds2
+        if count2 != 0:        
+            s = _('%(number)d %(type)s, %(number2)d %(type2)s ago') % {'number': count, 'type': name(count), 
+                                                                       'number2': count2, 'type2': name2(count2)}        
+    if not s:
+        s = _('%(number)d %(type)s ago') % {'number': count, 'type': name(count)}        
+    return s

Added: trunk/hotssh/hotlib_ui/__init__.py
==============================================================================

Added: trunk/hotssh/hotlib_ui/msgarea.py
==============================================================================
--- (empty file)
+++ trunk/hotssh/hotlib_ui/msgarea.py	Tue May 20 15:11:46 2008
@@ -0,0 +1,235 @@
+# This file is part of the Hotwire Shell user interface.
+#   
+# Copyright (C) 2007,2008 Colin Walters <walters verbum org>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+import os, sys, re, logging, string
+
+import gtk, gobject, pango
+
+from hotssh.hotlib.logutil import log_except
+
+_logger = logging.getLogger("hotwire.ui.MsgArea")
+
+# This file is a Python translation of gedit/gedit/gedit-message-area.c
+
+class MsgArea(gtk.HBox):
+    __gsignals__ = {
+        "response" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_INT,)),                    
+        "close" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, [])
+    }
+
+    def __init__(self, buttons, **kwargs):
+        super(MsgArea, self).__init__(**kwargs)
+        
+        self.__contents = None
+        self.__changing_style = False
+        
+        self.__main_hbox = gtk.HBox(False, 16) # FIXME: use style properties
+        self.__main_hbox.show()
+        self.__main_hbox.set_border_width(8) # FIXME: use style properties
+
+        self.__action_area = gtk.HBox(True, 4); # FIXME: use style properties
+        self.__action_area.show()
+        self.__main_hbox.pack_end (self.__action_area, False, True, 0)
+
+        self.pack_start(self.__main_hbox, True, True, 0)
+
+        self.set_app_paintable(True)
+
+        self.connect("expose-event", self.__paint)
+
+        # Note that we connect to style-set on one of the internal
+        # widgets, not on the message area itself, since gtk does
+        # not deliver any further style-set signals for a widget on
+        # which the style has been forced with gtk_widget_set_style()
+        self.__main_hbox.connect("style-set", self.__on_style_set)
+        
+        self.add_buttons(buttons)       
+
+    def __get_response_data(self, w, create):
+        d = w.get_data('hotwire-msg-area-data')
+        if (d is None) and create:
+            d = {'respid': None}
+            w.set_data('hotwire-msg-area-data', d)
+        return d
+    
+    def __find_button(self, respid):
+        children = self.__actionarea.get_children()
+        for child in children:
+            rd = self.__get_response_data(child, False)
+            if rd is not None and rd['respid'] == respid:
+                return child
+    
+    def __close(self):
+        cancel = self.__find_button(gtk.RESPONSE_CANCEL)
+        if cancel is None: 
+            return
+        self.response(gtk.RESPONSE_CANCEL)
+        
+    def __paint(self, w, event):
+        gtk.Style.paint_flat_box(w.style,
+                                 w.window,
+                                 gtk.STATE_NORMAL,
+                                 gtk.SHADOW_OUT,
+                                 None,
+                                 w,
+                                 "tooltip",
+                                 w.allocation.x + 1,
+                                 w.allocation.y + 1,
+                                 w.allocation.width - 2,
+                                 w.allocation.height - 2)
+    
+        return False
+    
+    def __on_style_set(self, w, style):
+        if self.__changing_style:
+            return
+        # This is a hack needed to use the tooltip background color
+        window = gtk.Window(gtk.WINDOW_POPUP);
+        window.set_name("gtk-tooltip")
+        window.ensure_style()
+        style = window.get_style()
+
+        self.__changing_style = True
+        self.set_style(style)
+        self.__changing_style = False
+
+        window.destroy()
+
+        self.queue_draw()
+        
+    def __get_response_for_widget(self, w):
+        rd = self.__get_response_data(w, False)
+        if rd is None:
+            return gtk.RESPONSE_NONE
+        return rd['respid']
+    
+    def __on_action_widget_activated(self, w):
+        response_id = self.__get_response_for_widget(w)
+        self.response(response_id)
+        
+    def add_action_widget(self, child, respid):
+        rd = self.__get_response_data(child, True)
+        rd['respid'] = respid
+        if not isinstance(child, gtk.Button):
+            raise ValueError("Can only pack buttons as action widgets")
+        child.connect('clicked', self.__on_action_widget_activated)
+        if respid != gtk.RESPONSE_HELP:
+            self.__action_area.pack_start(child, False, False, 0)
+        else:
+            self.__action_area.pack_end(child, False, False, 0)
+            
+    def set_contents(self, contents):
+        self.__contents = contents
+        self.__main_hbox.pack_start(contents, True, True, 0)
+        
+        
+    def add_button(self, btext, respid):
+        button = gtk.Button(stock=btext)
+        button.set_focus_on_click(False)
+        button.set_flags(gtk.CAN_DEFAULT)
+        button.show()
+        self.add_action_widget(button, respid)
+        return button
+    
+    def add_buttons(self, args):
+        _logger.debug("init buttons: %r", args)
+        for (btext, respid) in args:
+            self.add_button(btext, respid)
+    
+    def set_response_sensitive(self, respid, setting):
+        for child in self.__action_area.get_children():
+            rd = self.__get_response_data(child, False)
+            if rd is not None and rd['respid'] == respid:
+                child.set_sensitive(setting)
+                break
+            
+    def set_default_response(self, respid):
+        for child in self.__action_area.get_children():
+            rd = self.__get_response_data(child, False)
+            if rd is not None and rd['respid'] == respid:
+                child.grab_default()
+                break        
+            
+    def response(self, respid):
+        self.emit('response', respid)
+        
+    def add_stock_button_with_text(self, text, stockid, respid):
+        b = gtk.Button(label=text)
+        b.set_focus_on_click(False)
+        img = gtk.Image()
+        img.set_from_stock(stockid, gtk.ICON_SIZE_BUTTON)
+        b.set_image(img)
+        b.show_all()
+        self.add_action_widget(b, respid)
+        return b
+
+    def set_text_and_icon(self, stockid, primary_text, secondary_text=None):
+        hbox_content = gtk.HBox(False, 8)
+        hbox_content.show()
+
+        image = gtk.Image()
+        image.set_from_stock(stockid, gtk.ICON_SIZE_BUTTON)
+        image.show()
+        hbox_content.pack_start(image, False, False, 0)
+        image.set_alignment(0.5, 0.5)
+    
+        vbox = gtk.VBox(False, 6)
+        vbox.show()
+        hbox_content.pack_start (vbox, True, True, 0)
+    
+        primary_markup = "<b>%s</b>" % (primary_text,)
+        primary_label = gtk.Label(primary_markup)
+        primary_label.show()
+        vbox.pack_start(primary_label, True, True, 0)
+        primary_label.set_use_markup(True)
+        primary_label.set_line_wrap(True)
+        primary_label.set_alignment(0, 0.5)
+        primary_label.set_flags(gtk.CAN_FOCUS)
+        primary_label.set_selectable(True)
+    
+        if secondary_text:
+            secondary_markup = "<small>%s</small>" % (secondary_text,)
+            secondary_label = gtk.Label(secondary_markup)
+            secondary_label.show()
+            vbox.pack_start(secondary_label, True, True, 0)
+            secondary_label.set_flags(gtk.CAN_FOCUS)
+            secondary_label.set_use_markup(True)
+            secondary_label.set_line_wrap(True)
+            secondary_label.set_selectable(True)
+            secondary_label.set_alignment(0, 0.5)
+    
+        self.set_contents(hbox_content)
+
+class MsgAreaController(gtk.HBox):
+    def __init__(self):
+        super(MsgAreaController, self).__init__()
+        
+        self.__msgarea = None
+        
+    def clear(self):
+        if self.__msgarea is not None:
+            self.remove(self.__msgarea)
+            self.__msgarea.destroy()
+            self.__msgarea = None
+        
+    def new_from_text_and_icon(self, stockid, primary, secondary=None, buttons=[]):
+        self.clear()
+        msgarea = self.__msgarea = MsgArea(buttons)
+        msgarea.set_text_and_icon(stockid, primary, secondary)
+        self.pack_start(msgarea, expand=True)
+        return msgarea

Added: trunk/hotssh/hotlib_ui/quickfind.py
==============================================================================
--- (empty file)
+++ trunk/hotssh/hotlib_ui/quickfind.py	Tue May 20 15:11:46 2008
@@ -0,0 +1,216 @@
+# This file is part of the Hotwire Shell user interface.
+#   
+# Copyright (C) 2007 Colin Walters <walters verbum org>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+import os, sys, re, logging, string
+
+import gtk, gobject, pango
+
+from hotssh.hotlib.logutil import log_except
+
+def markup_for_match(text, start, end, matchtarget=None):
+    source = matchtarget or text
+    return  '%s<b>%s</b>%s%s' % (gobject.markup_escape_text(source[0:start]),
+                                 gobject.markup_escape_text(source[start:end]),
+                                 gobject.markup_escape_text(source[end:]),
+                                 matchtarget and (' - <i>' + text + '</i>') or '')
+
+_logger = logging.getLogger("hotwire.ui.QuickFind")
+
+class QuickFindWindow(gtk.Dialog):
+    def __init__(self, title, parent=None):
+        super(QuickFindWindow, self).__init__(title=title,
+                                              parent=parent,
+                                              flags=gtk.DIALOG_DESTROY_WITH_PARENT,
+                                              buttons=(gtk.STOCK_CLOSE, gtk.RESPONSE_ACCEPT))
+        
+        self.connect('response', lambda *args: self.hide())
+        self.connect('delete-event', self.hide_on_delete)
+                
+        self.set_has_separator(False)
+        self.set_border_width(5)
+        
+        self.__vbox = gtk.VBox()
+        self.vbox.add(self.__vbox)   
+        self.vbox.set_spacing(6)
+        
+        self.set_size_request(640, 480)
+   
+        self.__response_value = None
+   
+        self.__idle_search_id = 0
+   
+        self.__entry = gtk.Entry()
+        self.__entry.connect('notify::text', self.__on_text_changed)
+        self.__entry.connect('key-press-event', self.__on_keypress)
+        self.__vbox.pack_start(self.__entry, expand=False)
+        self.__scroll = gtk.ScrolledWindow()
+        self.__scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+        self.__results = gtk.TreeView()
+        self.__results.connect('row-activated', self.__on_row_activated)
+        self.__scroll.add(self.__results)
+        colidx = self.__results.insert_column_with_data_func(-1, '',
+                                                             gtk.CellRendererPixbuf(),
+                                                             self.__render_icon)
+        colidx = self.__results.insert_column_with_data_func(-1, '',
+                                                             hotwidgets.CellRendererText(ellipsize=True),
+                                                             self.__render_match)        
+        self.__vbox.pack_start(hotwidgets.Border(self.__scroll), expand=True)
+        self.__selection = self.__results.get_selection()
+        self.__selection.set_mode(gtk.SELECTION_SINGLE)
+        self.__selection.connect('changed', self.__on_selection_changed)
+        self.__results.set_headers_visible(False)
+        
+        self.__do_search()
+        
+    def __on_keypress(self, entry, event):
+        if event.keyval == gtk.gdk.keyval_from_name('Return'):
+            self.__idle_do_search()
+            return self.__activate_selection()
+        elif event.keyval == gtk.gdk.keyval_from_name('Up'):
+            self.__idle_do_search()
+            self.__select_next()
+            return True
+        elif event.keyval == gtk.gdk.keyval_from_name('Down'):
+            self.__idle_do_search()
+            self.__select_prev()
+            return True
+        return False        
+        
+    def __on_text_changed(self, *args):
+        if self.__idle_search_id > 0:
+            return
+        self.__idle_search_id = gobject.timeout_add(300, self.__idle_do_search)
+        
+    def __idle_do_search(self):
+        if self.__idle_search_id == 0:
+            return False
+        self.__idle_search_id = 0
+        self.__do_search()
+        return False
+        
+    def _markup_search(self, text, searchq, text_lower=None, searchq_lower=None):
+        if text_lower is not None:
+            idx = text_lower.find(searchq_lower)
+        else:
+            idx = text.find(searchq)
+        if idx >= 0:
+            return markup_for_match(text, idx, idx+len(searchq))
+        else:
+            return None          
+        
+    def _get_string_data(self):
+        """Generate a list of strings for search data."""
+        raise NotImplementedError()
+    
+    def _do_search(self, text):
+        """Override this to implement a custom search with icons, etc."""
+        strings = self._get_string_data()
+        text = text.lower()
+        for s in strings:
+            if s.lower().find(text) >= 0:
+                yield (s, s, None)
+        
+    def __do_search(self):
+        text = self.__entry.get_property('text')
+        results = self._do_search(text)
+        model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)
+        have_results = False
+        for result in results:
+            have_results = True
+            _logger.debug("appending result: %s", result)
+            model.append(result)
+        self.__results.set_model(model)
+        if have_results:
+            self.__selection.select_iter(model.get_iter_first())            
+        
+    def __on_selection_changed(self, sel):
+        (model, iter) = sel.get_selected()
+        if iter:
+            self.__results.scroll_to_cell(model.get_path(iter))
+            
+    def __get_selected_path(self):
+        (model, iter) = self.__selection.get_selected()
+        return iter and model.get_path(iter)
+        
+    def __select_next(self):
+        path = self.__get_selected_path()
+        if not path:
+            return
+        previdx = path[-1]-1
+        if previdx < 0:
+            return
+        model = self.__results.get_model()        
+        previter = model.iter_nth_child(None, previdx)
+        if not previter:
+            return
+        self.__selection.select_iter(previter)
+        
+    def __select_prev(self):
+        path = self.__get_selected_path()
+        if not path:
+            return
+        model = self.__results.get_model()        
+        seliter = model.get_iter(path)
+        iternext = model.iter_next(seliter)
+        if not iternext:
+            return
+        self.__selection.select_iter(iternext)
+        
+    def __activate_selection(self):
+        (model, iter) = self.__selection.get_selected()
+        if not iter:
+            return False
+        self._handle_activation(model.get_value(iter, 0))
+        return True        
+        
+    @log_except(_logger)
+    def __on_row_activated(self, tv, path, vc):
+        _logger.debug("row activated: %s", path)
+        model = self.__results.get_model()
+        iter = model.get_iter(path)
+        self._handle_activation(model.get_value(iter, 0))
+    
+    def __render_match(self, col, cell, model, iter):
+        markup = model.get_value(iter, 1)
+        if markup:
+            cell.set_property('markup', markup)
+        else:
+            cell.set_property('text', model.get_value(iter, 0))    
+    
+    def __render_icon(self, col, cell, model, iter):
+        icon_name = model.get_value(iter, 2)
+        if icon_name:
+            pbcache = PixbufCache.getInstance()
+            pixbuf = pbcache.get(icon_name, size=16, trystock=True, stocksize=gtk.ICON_SIZE_MENU)
+            cell.set_property('pixbuf', pixbuf)
+        else:
+            cell.set_property('icon-name', None)    
+    
+    def get_response_value(self):
+        return self.__response_value
+        
+    def _handle_activation(self, val):
+        self.__response_value = val
+        self.response(gtk.RESPONSE_ACCEPT)
+    
+    def run_get_value(self):
+        self.show_all()
+        resp = self.run()
+        if resp == gtk.RESPONSE_ACCEPT:
+            return self.__response_value
+        return None

Added: trunk/hotssh/hotvte/__init__.py
==============================================================================

Added: trunk/hotssh/hotvte/vteterm.py
==============================================================================
--- (empty file)
+++ trunk/hotssh/hotvte/vteterm.py	Tue May 20 15:11:46 2008
@@ -0,0 +1,264 @@
+# This file is part of the Hotwire Shell user interface.
+#   
+# Copyright (C) 2007 Colin Walters <walters verbum org>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+import os,sys,threading,pty,logging
+
+import gtk, gobject, pango
+import vte
+
+try:
+    import gconf
+    gconf_available = True
+except:
+    gconf_available = False
+
+_logger = logging.getLogger("hotssh.VteTerminal")
+
+class VteTerminalScreen(gtk.Bin):
+    def __init__(self):
+        super(VteTerminalScreen, self).__init__()
+        self.term = vte.Terminal()
+        self.__termbox = gtk.HBox()
+        self.__scroll = gtk.VScrollbar(self.term.get_adjustment())
+        border = gtk.Frame()
+        border.set_shadow_type(gtk.SHADOW_ETCHED_IN)
+        border.add(self.term)
+        self.__termbox.pack_start(border)
+        self.__termbox.pack_start(self.__scroll, False)
+        self.add(self.__termbox)
+
+    def do_size_request(self, req):
+        (w,h) = self.__termbox.size_request()
+        req.width = w
+        req.height = h
+
+    def do_size_allocate(self, alloc):
+        self.allocation = alloc
+        wid_req = self.__termbox.size_allocate(alloc)
+
+gobject.type_register(VteTerminalScreen)
+
+# From gnome-terminal src/terminal-screen.c
+_USERCHARS = "-A-Za-z0-9"
+_PASSCHARS = "-A-Za-z0-9,?;.:/!%$^*&~\"#'"
+_HOSTCHARS = "-A-Za-z0-9"
+_PATHCHARS = "-A-Za-z0-9_$.+!*(),;:@&=?/~#%"
+_SCHEME = "(news:|telnet:|nntp:|file:/|https?:|ftps?:|webcal:)"
+_USER = "[" + _USERCHARS + "]+(:[" + _PASSCHARS + "]+)?"
+_URLPATH = "/[" + _PATHCHARS + "]*[^]'.}>) \t\r\n,\\\"]"
+
+class VteTerminalWidget(gtk.VBox):
+    __gsignals__ = {
+        "child-exited" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+        "fork-child" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),        
+    }    
+    def __init__(self, cwd=None, cmd=None, ptyfd=None, initbuf=None, **kwargs):
+        super(VteTerminalWidget, self).__init__()
+        
+        self.__screen = screen = VteTerminalScreen()
+        self.__term = screen.term
+        self.pack_start(screen, expand=True)
+        
+        self.pid = None
+        self.exited = False
+        
+        self.__actions = [
+            ('Copy', 'gtk-copy', _('_Copy'), '<control><shift>C', _('Copy selected text'), self.__copy_cb),
+            ('Paste', 'gtk-paste', _('_Paste'), '<control><shift>V', _('Paste text'), self.__paste_cb),
+        ]
+        self.__action_group = gtk.ActionGroup('TerminalActions')
+        self.__action_group.add_actions(self.__actions)
+        self.__copyaction = self.__action_group.get_action('Copy')
+        self.__pasteaction = self.__action_group.get_action('Paste')         
+
+        # Various defaults
+        self.__term.set_emulation('xterm')
+        self.__term.set_allow_bold(True)
+        self.__term.set_size(80, 24)
+        self.__term.set_scrollback_lines(1500)
+        self.__term.set_mouse_autohide(True)
+        
+        self.__colors_default = True
+        self.__term.set_default_colors()
+        if gconf_available:
+            self.__set_gnome_colors()
+        else:
+            self.__set_gtk_colors()        
+
+        # Use Gnome font if available
+        if gconf_available:
+            gconf_client = gconf.client_get_default()
+            def on_font_change(*args):
+                mono_font = gconf_client.get_string('/desktop/gnome/interface/monospace_font_name')
+                _logger.debug("Using font '%s'", mono_font)
+                font_desc = pango.FontDescription(mono_font)
+                self.__term.set_font(font_desc)                
+            gconf_client.notify_add('/desktop/gnome/interface/monospace_font_name', on_font_change)
+            on_font_change()
+            
+        self.__match_asis = self.__term.match_add("\\<" + _SCHEME + "//(" + _USER + "@)?[" + _HOSTCHARS + ".]+" + \
+                                                  "(:[0-9]+)?(" + _URLPATH + ")?\\>/?")
+
+        self.__match_http = self.__term.match_add("\\<(www|ftp)[" + _HOSTCHARS + "]*\\.["  + _HOSTCHARS + ".]+" + \
+                                                  "(:[0-9]+)?(" + _URLPATH + ")?\\>/?")
+        
+        self.__term.connect('button-press-event', self.__on_button_press)
+        self.__term.connect('selection-changed', self.__on_selection_changed)
+        self.__on_selection_changed()
+
+        if ptyfd is not None:
+            # If we have a PTY, set it up immediately
+            self.__idle_do_cmd_fork(None, cwd, ptyfd, initbuf)
+        else:
+            # http://code.google.com/p/hotwire-shell/issues/detail?id=35
+            # We do the command in an idle to hopefully have more state set up by then;
+            # For example, "top" seems to be sized correctly on the first display
+            # this way            
+            gobject.timeout_add(250, self.__idle_do_cmd_fork, cmd, cwd, ptyfd, initbuf)
+            
+    def __idle_do_cmd_fork(self, cmd, cwd, ptyfd, initbuf):
+        _logger.debug("Forking cmd: %s", cmd)
+        self.__term.connect("child-exited", self._on_child_exited)
+        if cwd:
+            kwargs = {'directory': cwd}
+        else:
+            kwargs = {}
+        if ptyfd:
+            self.__term.set_pty(ptyfd)
+            pid = None
+        elif cmd:
+            pid = self.__term.fork_command(cmd[0], cmd, **kwargs)
+        else:
+            pid = self.__term.fork_command(**kwargs)
+        if initbuf is not None:
+            self.__term.feed(initbuf)            
+        self.pid = pid
+        self.emit('fork-child')
+        
+    def __on_button_press(self, term, event):
+        match = self.__term.match_check(int(event.x/term.get_char_width()), int(event.y/term.get_char_height()))
+        if event.button == 1 and event.state & gtk.gdk.CONTROL_MASK:
+            if not match:
+                return
+            (matchstr, mdata) = match            
+            if mdata == self.__match_http:
+                url = 'http://' + matchstr
+            else:
+                url = matchstr
+            self.__open_url(url)
+            return True
+        elif event.button == 3:
+            menu = gtk.Menu()
+            menuitem = self.__copyaction.create_menu_item()
+            menu.append(menuitem)
+            menuitem = self.__pasteaction.create_menu_item()
+            menu.append(menuitem)
+            if match:
+                (matchstr, mdata) = match
+                menuitem = gtk.ImageMenuItem(_('Open Link'))
+                menuitem.set_property('image', gtk.image_new_from_stock('gtk-go-to', gtk.ICON_SIZE_MENU))
+                menuitem.connect('activate', lambda menu: self.__open_url(url))
+                menu.append(gtk.SeparatorMenuItem())
+                menu.append(menuitem)
+            extra = self._get_extra_context_menuitems()
+            if len(extra) > 0:
+                menu.append(gtk.SeparatorMenuItem())
+                for item in extra:
+                    menu.append(item)
+            menu.show_all()
+            menu.popup(None, None, None, event.button, event.time)            
+            return True
+        return False
+
+    def _get_extra_context_menuitems(self):
+        return []
+
+    def __open_url(self, url):
+        # Older webbrowser.py didn't check gconf
+        from hotwire.sysdep import is_windows
+        if sys.version_info[0] == 2 and sys.version_info[1] < 6 and (not is_windows()):
+            try:
+                import hotwire.externals.webbrowser as webbrowser
+            except ImportError, e:
+                _logger.warn("Couldn't import hotwire.externals.webbrowser", exc_info=True)
+                import webbrowser
+        else:
+            import webbrowser            
+        webbrowser.open(url)        
+            
+    def __on_selection_changed(self, *args):
+        have_selection = self.__term.get_has_selection()
+        self.__copyaction.set_sensitive(have_selection)
+
+    def __copy_cb(self, a):
+        _logger.debug("doing copy")
+        self.__term.copy_clipboard()
+
+    def __paste_cb(self, a):
+        _logger.debug("doing paste")        
+        self.__term.paste_clipboard()            
+            
+    def _on_child_exited(self, term):
+        _logger.debug("Caught child exited")
+        self.exited = True
+        self.emit('child-exited')        
+   
+    def get_vte(self):
+        return self.__term
+    
+    def get_action_group(self):
+        return self.__action_group
+    
+    def set_copy_paste_actions(self, copyaction, pasteaction):
+        """Useful in an environment where there is a global UI
+        rather than a merged approach.""" 
+        self.__copyaction = copyaction
+        self.__pasteaction = pasteaction
+        
+    def __set_gnome_colors(self):
+        gconf_client = gconf.client_get_default()        
+        term_profile = '/apps/gnome-terminal/profiles/Default'
+        fg_key = term_profile + '/foreground_color'
+        bg_key = term_profile + '/background_color'
+        def on_color_change(*args):
+            if not self.__colors_default:
+                return
+            fg = gtk.gdk.color_parse(gconf_client.get_string(fg_key))
+            self.set_color(True, fg, isdefault=True)
+            bg = gtk.gdk.color_parse(gconf_client.get_string(bg_key))
+            self.set_color(False, bg, isdefault=True)
+        gconf_client.notify_add(fg_key, on_color_change)
+        gconf_client.notify_add(bg_key, on_color_change)
+        on_color_change()        
+    
+    def __set_gtk_colors(self):
+        fg = self.style.text[gtk.STATE_NORMAL]
+        bg = self.style.base[gtk.STATE_NORMAL]
+        self.set_color(True, fg, isdefault=True)
+        self.set_color(False, bg, isdefault=True)
+        
+    def set_color(self, is_foreground, color, isdefault=False):
+        if not isdefault:
+            self.__colors_default = False            
+        if is_foreground:
+            self.__term.set_color_foreground(color)
+            self.__term.set_color_bold(color)
+            self.__term.set_color_dim(color)            
+        else:
+            self.__term.set_color_background(color)
+

Added: trunk/hotssh/hotvte/vtewindow.py
==============================================================================
--- (empty file)
+++ trunk/hotssh/hotvte/vtewindow.py	Tue May 20 15:11:46 2008
@@ -0,0 +1,543 @@
+# This file is part of the Hotwire Shell user interface.
+#   
+# Copyright (C) 2007 Colin Walters <walters verbum org>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+import os,sys,platform,logging,getopt
+import locale,threading,subprocess,time
+import signal
+
+import gtk,gobject,pango
+import dbus,dbus.glib,dbus.service
+
+from hotssh.hotvte.vteterm import VteTerminalWidget
+
+from hotssh.hotlib_ui.quickfind import QuickFindWindow
+
+_logger = logging.getLogger("hotvte.VteWindow")
+
+class QuickSwitchTabWindow(QuickFindWindow):
+    def __init__(self, vtewin):
+        self.__vtewin = vtewin        
+        super(QuickSwitchTabWindow, self).__init__(_('Tab Search'),
+                                                   parent=vtewin)
+        
+    def _do_search(self, text):
+        for widget in self.__vtewin.get_tabs():
+            title = widget.get_title()
+            markup = self._markup_search(title, text)
+            if markup is not None:
+                yield (title, markup, None)
+
+class TabbedVteWidget(VteTerminalWidget):
+    def __init__(self, cmd=None, *args, **kwargs):
+        super(TabbedVteWidget, self).__init__(cmd=cmd, *args, **kwargs)
+        self.__title = ' '.join(cmd)
+        
+    def get_title(self):
+        return self.__title
+
+class VteWindow(gtk.Window):
+    ascii_nums = [long(x+ord('0')) for x in xrange(10)]    
+    def __init__(self, factory=None, title=None, icon_name=None, **kwargs):
+        super(VteWindow, self).__init__()
+        
+        self.__factory = factory
+        
+        self.__idle_save_session_id = 0
+        
+        self.__old_char_height = 0
+        self.__old_char_width = 0
+        self.__old_geom_widget = None
+        
+        vbox = gtk.VBox()
+        self.add(vbox)
+        self.__ui_string = """
+<ui>
+  <menubar name='Menubar'>
+    <menu action='FileMenu'>
+      <placeholder name='FileAdditions'/>
+      <separator/>
+      <menuitem action='TabSearch'/>      
+      <menuitem action='DetachTab'/>
+      <separator/>
+      <menuitem action='Close'/>
+    </menu>
+    <menu action='EditMenu'>
+      <menuitem action='Copy'/>
+      <menuitem action='Paste'/>
+      <separator/>
+      <placeholder name='EditAdditions'/>      
+    </menu>
+    <menu action='ViewMenu'>
+      <separator/>
+      <placeholder name='ViewAdditions'/>        
+    </menu>
+    <placeholder name='TermAppAdditions'/>
+    <menu action='ToolsMenu'>
+      <menuitem action='About'/>
+    </menu>
+  </menubar>
+</ui>
+"""       
+        self.__create_ui()
+        vbox.pack_start(self.__ui.get_widget('/Menubar'), expand=False)
+        
+        self.connect("key-press-event", self.__on_keypress)
+                
+        self.__title = title
+        self.set_title(title)
+        self.set_default_size(720, 540)
+        if os.getenv('HOTWIRE_UNINSTALLED'):
+            # For some reason set_icon() started failing...do it manually.
+            iinf = gtk.icon_theme_get_default().lookup_icon(icon_name, 24, 0)
+            if iinf:
+                fn = iinf.get_filename()
+                self.set_icon_from_file(fn)
+        else:
+            self.set_icon_name(icon_name)
+        self.connect("delete-event", lambda w, e: False)
+        self.__tips = gtk.Tooltips()
+        self.__notebook = gtk.Notebook()
+        vbox.pack_start(self.__notebook)
+        self.__notebook.connect('switch-page', self.__on_page_switch)
+        self.__notebook.set_scrollable(True)
+        self.__notebook.show()
+        
+        self.__tabs_visible = self.__notebook.get_show_tabs()
+        self.__queue_session_save()
+        
+    def __queue_session_save(self):
+        self.__factory.queue_save_session()        
+        
+    def new_tab(self, args, cwd):
+        widget = TabbedVteWidget(cmd=args, cwd=cwd)
+        return self.append_widget(widget)
+        
+    def remote_new_tab(self, args, cwd):
+        return self.new_tab(args, cwd)
+        
+    def append_widget(self, term):
+        idx = self.__notebook.append_page(term)
+        term.get_vte().connect('selection-changed', self.__sync_selection_sensitive)
+        term.get_term().set_copy_paste_actions(self.__ag.get_action('Copy'), self.__ag.get_action('Paste'))
+        if hasattr(term, 'has_close'):
+            has_close = term.has_close()
+        else:
+            has_close = False
+        if has_close:
+            term.connect('close', self.__on_widget_close)
+        if hasattr(self.__notebook, 'set_tab_reorderable'):
+            self.__notebook.set_tab_reorderable(term, True)
+        label = self.__add_widget_title(term)
+        title = term.get_title()
+        label.set_text(title)
+        self.__tips.set_tip(label, title)
+        term.show_all()
+        self.__notebook.set_current_page(idx)
+        term.get_vte().grab_focus()
+        return term
+        
+    def _get_notebook(self):
+        return self.__notebook
+
+    def __on_page_switch(self, n, p, pn):
+        _logger.debug("got page switch, pn=%d", pn)
+        widget = self.__notebook.get_nth_page(pn)
+        term = widget.get_vte()
+        (cw, ch, (xp, yp)) = (term.get_char_width(), term.get_char_height(), term.get_padding())
+        if not (cw == self.__old_char_width and ch == self.__old_char_height and widget == self.__old_geom_widget):
+            _logger.debug("resetting geometry on %s %s %s => %s %s", widget, self.__old_char_width, self.__old_char_height, cw, ch)
+            kwargs = {'base_width':xp,
+                      'base_height':yp,
+                      'width_inc':cw,
+                      'height_inc':ch,
+                      'min_width':xp+cw*4,
+                      'min_height':yp+ch*2}
+            _logger.debug("setting geom hints: %s", kwargs)
+            self.__geom_hints = kwargs
+            self.set_geometry_hints(widget, **kwargs)
+            self.__old_char_width = cw
+            self.__old_char_height = ch
+            self.__old_geom_widget = widget
+            
+        self.__sync_selection_sensitive()
+        self.set_title('%s - %s' % (widget.get_title(),self.__title))
+        self.__queue_session_save()
+        
+    def __on_keypress(self, s2, e):
+        if e.keyval == gtk.gdk.keyval_from_name('Page_Up') and \
+             e.state & gtk.gdk.CONTROL_MASK:
+            idx = self.__notebook.get_current_page() 
+            self.__notebook.set_current_page(idx-1)
+            return True
+        elif e.keyval == gtk.gdk.keyval_from_name('Page_Down') and \
+             e.state & gtk.gdk.CONTROL_MASK:
+            idx = self.__notebook.get_current_page() 
+            self.__notebook.set_current_page(idx+1)
+            return True
+        elif e.keyval in VteWindow.ascii_nums and \
+             e.state & gtk.gdk.MOD1_MASK:
+            self.__notebook.set_current_page(e.keyval-ord('0')-1) #extra -1 because tabs are 0-indexed
+            return True
+        elif e.keyval == gtk.gdk.keyval_from_name('Return'):
+            widget = self.__notebook.get_nth_page(self.__notebook.get_current_page())        
+            if widget.get_exited():
+                self.__close_tab(widget)
+                return True
+        return False        
+        
+    def __create_ui(self):
+        self.__using_accels = True
+        self.__ag = ag = gtk.ActionGroup('WindowActions')
+        self.__actions = actions = [
+            ('FileMenu', None, _('File')),
+            ('DetachTab', None, _('_Detach Tab'), '<control><shift>D', 'Move tab into new window', self.__detach_cb),
+            ('TabSearch', None, '_Search Tabs', '<control><shift>L', 'Search across tab names', self.__quickswitch_tab_cb),
+            ('Close', gtk.STOCK_CLOSE, _('_Close'), '<control><shift>W',
+             'Close the current tab', self.__close_cb),
+            ('EditMenu', None, _('Edit')),
+            ('Copy', 'gtk-copy', _('_Copy'), '<control><shift>C', 'Copy selected text', self.__copy_cb),
+            ('Paste', 'gtk-paste', _('_Paste'), '<control><shift>V', 'Paste text', self.__paste_cb),                   
+            ('ViewMenu', None, _('View')),
+            ('ToolsMenu', None, _('Tools')),                    
+            ('About', gtk.STOCK_ABOUT, _('_About'), None, 'About HotVTE', self.__help_about_cb),
+            ]
+        ag.add_actions(actions)
+        self.__ui = gtk.UIManager()
+        self.__ui.insert_action_group(ag, 0)
+        self.__ui.add_ui_from_string(self.__ui_string)
+        self.add_accel_group(self.__ui.get_accel_group())
+        
+    def _merge_ui(self, actions, uistr):
+        self.__ag.add_actions(actions)
+        self.__ui_merge_id = self.__ui.add_ui_from_string(uistr)
+        return self.__ag      
+    
+    def _get_ui(self):
+        return self.__ui   
+
+    def __add_widget_title(self, w):
+        hbox = gtk.HBox()
+        label = gtk.Label('<notitle>')
+        label.set_selectable(False)
+        label.set_ellipsize(pango.ELLIPSIZE_END)
+        hbox.pack_start(label, expand=True)
+
+        close = gtk.Button()
+        close.set_focus_on_click(False)
+        close.set_relief(gtk.RELIEF_NONE)
+        close.set_name('hotwire-tab-close')
+        img = gtk.Image()
+        img.set_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU)
+        close.add(img)
+        close.connect('clicked', lambda b: self.__close_tab(w))        
+        (width, height) = gtk.icon_size_lookup_for_settings(label.get_settings(), gtk.ICON_SIZE_MENU)
+        close.set_size_request(width + 2, height + 2)
+        hbox.pack_start(close, expand=False)
+        hbox.show_all()
+        self.__notebook.set_tab_label(w, hbox)
+        w.set_data('hotwire-tab-label', label)
+        self.__notebook.set_tab_label_packing(w, True, True, gtk.PACK_START)
+        self.__sync_tabs_visible()
+        return label
+    
+    def __close_tab(self, w):
+        self.__remove_page_widget(w)
+        w.destroy()
+        
+    def __close_cb(self, action):
+        self.__remove_page_widget(self.__notebook.get_nth_page(self.__notebook.get_current_page()))
+        
+    def __on_widget_close(self, widget):
+        self.__remove_page_widget(widget)
+
+    def __help_about_cb(self, action):
+        from hotwire_ui.aboutdialog import HotwireAboutDialog        
+        dialog = HotwireAboutDialog()
+        dialog.run()
+        dialog.destroy()
+        
+    def get_tabs(self):
+        return self.__notebook.get_children()
+        
+    def __quickswitch_tab_cb(self, action):
+        w = QuickSwitchTabWindow(self)
+        tabtitle = w.run_get_value()
+        if tabtitle is None:
+            return
+        _logger.debug("got switch title: %r", tabtitle)
+        for child in self.__notebook.get_children():
+            if child.get_title() == tabtitle:
+                self.__notebook.set_current_page(self.__notebook.page_num(child))
+                break
+        
+    def __sync_tabs_visible(self):
+        multitab = self.__notebook.get_n_pages() > 1
+        self.__ag.get_action('DetachTab').set_sensitive(multitab)
+        self.__ag.get_action('TabSearch').set_sensitive(multitab)        
+        self.__notebook.set_show_tabs(multitab)        
+        
+    def __remove_page_widget(self, w):
+        idx = self.__notebook.page_num(w)
+        _logger.debug("tab closed, current: %d", idx)
+        self.__notebook.remove_page(idx)
+        self.__sync_tabs_visible()
+        if self.__notebook.get_n_pages() == 0:
+            self.destroy()      
+
+    def __sync_selection_sensitive(self, *args):
+        have_selection = self.__notebook.get_nth_page(self.__notebook.get_current_page()).get_vte().get_has_selection()
+        self.__ag.get_action('Copy').set_sensitive(have_selection)
+
+    def __copy_cb(self, a):
+        _logger.debug("doing copy")
+        widget = self.__notebook.get_nth_page(self.__notebook.get_current_page())        
+        widget.get_vte().copy_clipboard()        
+
+    def __paste_cb(self, a):
+        _logger.debug("doing paste")
+        widget = self.__notebook.get_nth_page(self.__notebook.get_current_page())
+        widget.get_vte().paste_clipboard()
+
+    def __detach_cb(self, a):
+        widget = self.__notebook.get_nth_page(self.__notebook.get_current_page())
+        self.__remove_page_widget(widget)               
+        win = self.__factory.create_window()
+        win.append_widget(widget)
+        win.show_all()
+                
+_factory = None
+class VteWindowFactory(gobject.GObject):
+    __gsignals__ = {
+        "shutdown" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+    }    
+    def __init__(self, klass, window_args, app):
+        super(VteWindowFactory, self).__init__()
+        self.__windows = set()
+        self.__klass = klass
+        self.__app = app
+        self.__window_args = window_args
+        self.__sticky_keywords = {'subtitle': ''}
+        self.__sticky_keywords.update(window_args)
+        self.__recentwindow = None
+        self.__idle_save_session_id = 0        
+        
+    @staticmethod
+    def getInstance():
+        global _factory
+        if _factory is None:
+            _factory = VteWindowFactory()
+        return _factory
+
+    def offer_load_session(self):
+        dlg = Load
+
+    def create_initial_window(self, *args, **kwargs):
+        win = self.create_window(is_initial=True, *args, **kwargs)
+        self.__recentwindow = win
+        return win
+
+    def create_window(self, is_initial=False, *args, **kwargs):
+        _logger.debug("creating window")
+        if is_initial:
+            for k,v in kwargs.iteritems():
+                if self.__sticky_keywords.has_key(k):
+                    self.__sticky_keywords[k] = v
+        for k,v in self.__sticky_keywords.iteritems():
+            if not kwargs.has_key(k):
+                kwargs[k] = v
+        win = self.__klass(factory=self, is_initial=is_initial, **kwargs)
+        win.connect('notify::is-active', self.__on_window_active)
+        win.connect('destroy', self.__on_win_destroy)
+        self.__windows.add(win)
+        return win
+    
+    def remote_new_tab(self, cmd, cwd):
+        self.__recentwindow.remote_new_tab(cmd, cwd)
+        return self.__recentwindow
+    
+    def queue_save_session(self):      
+        if self.__idle_save_session_id == 0:
+            self.__idle_save_session_id = gobject.timeout_add(5*1000, self.__idle_save_session)
+            
+    def __idle_save_session(self):
+        self.__idle_save_session_id = 0
+        self.__app.save_session()
+    
+    def _get_windows(self):
+        return self.__windows
+    
+    def __on_window_active(self, win, *args):
+        active = win.get_property('is-active')
+        if active:
+            self.__recentwindow = win
+
+    def __on_win_destroy(self, win):
+        _logger.debug("got window destroy")
+        self.__windows.remove(win)
+        win.get_child().destroy()
+        if len(self.__windows) == 0:
+            self.emit('shutdown')
+            gtk.main_quit()
+
+class UiProxy(dbus.service.Object):
+    def __init__(self, factory, bus_name, ui_iface, ui_opath):
+        super(UiProxy, self).__init__(dbus.service.BusName(bus_name, bus=dbus.SessionBus()), ui_opath)
+        self.__winfactory = factory
+        # This is a disturbing hack.  But it works.
+        def RunCommand(self, timestamp, istab, cmd, cwd):
+            _logger.debug("Handling RunCommand method invocation ts=%r cmd=%r cwd=%r)", timestamp, cmd, cwd)
+            if istab:
+                curwin = self.__winfactory.remote_new_tab(cmd, cwd)
+            else:
+                raise NotImplementedError('can only create new tabs')
+            timestamp = long(timestamp)+1
+            if timestamp > 0:
+                _logger.debug("presenting with timestamp %r", timestamp)
+                curwin.present_with_time(timestamp)
+            else:
+                curwin.present()
+        setattr(UiProxy, 'RunCommand', dbus.service.method(ui_iface, in_signature='ubass')(RunCommand))                
+
+class VteRemoteControl(object):
+    def __init__(self, name, bus_name=None, ui_opath=None, ui_iface=None):
+        super(VteRemoteControl, self).__init__()
+        self.__bus_name = bus_name or ('org.hotwireshell.HotVTE.' + name)
+        self.__ui_opath = ui_opath or ('/hotvte/' + name + '/ui')
+        self.__ui_iface = ui_iface or (self.__bus_name + '.Ui')
+        
+    def __parse_startup_id(self):
+        startup_time = None
+        try:
+            startup_id_env = os.environ['DESKTOP_STARTUP_ID']
+        except KeyError, e:
+            startup_id_env = None
+        if startup_id_env:
+            idx = startup_id_env.find('_TIME')
+            if idx > 0:
+                idx += 5
+                startup_time = int(startup_id_env[idx:])
+        return startup_time        
+        
+    def single_instance(self, replace=False):
+        proxy = dbus.SessionBus().get_object('org.freedesktop.DBus', '/org/freedesktop/DBus')
+        flags = 1 | 4 # allow replacement | do not queue
+        if replace:
+            flags = flags | 2 # replace existing
+        _logger.debug("Requesting D-BUS name %s on session bus", self.__bus_name)            
+        if not proxy.RequestName(self.__bus_name, dbus.UInt32(flags)) in (1,4):
+            inst = dbus.SessionBus().get_object(self.__bus_name, self.__ui_opath)
+            inst_iface = dbus.Interface(inst, self.__ui_iface)
+            _logger.debug("Sending RunCommand to existing instance")
+            # TODO support choosing tab/window
+            starttime = self.__parse_startup_id()
+            inst_iface.RunCommand(dbus.UInt32(starttime or 0), True, dbus.Array(sys.argv[1:], signature="s"), os.getcwd())
+            sys.exit(0)
+            os._exit(0)
+        
+    def get_proxy(self, factory):
+        return UiProxy(factory, self.__bus_name, self.__ui_iface, self.__ui_opath)
+    
+class VteApp(object): 
+    def __init__(self, windowklass):
+        super(VteApp, self).__init__()
+        self.__windowklass = windowklass
+        self.__factory = None
+        
+    @staticmethod
+    def get_name():
+        raise NotImplementedError()
+        
+    def get_remote(self):
+        return VteRemoteControl(self.get_name())
+        
+    def get_factory(self):
+        if self.__factory is None:
+            self.__factory = VteWindowFactory(self.__windowklass, {}, self)
+        return self.__factory
+    
+    def offer_load_session(self):
+        pass
+    
+    def on_shutdown(self, factory):
+        pass
+    
+    def save_session(self):
+        _logger.debug("noop session save")
+        pass
+    
+class VteMain(object):
+    def main(self, appklass):
+
+        default_log_level = logging.WARNING
+        if 'HOTVTE_DEBUG' in os.environ:
+            default_log_level = logging.DEBUG
+ 
+        import hotssh.hotlib.logutil
+        mods = os.environ.get('HOTVTE_DEBUG_MODULES', '')
+        if mods:
+            mods = mods.split(',')
+        else:
+            mods = []
+        hotssh.hotlib.logutil.init(default_log_level, mods, '')
+
+        _logger.debug("logging initialized")
+
+        locale.setlocale(locale.LC_ALL, '')
+        import gettext
+        gettext.install(appklass.get_name())      
+    
+        gobject.threads_init()
+        
+        app = appklass()
+        remote = app.get_remote()
+        remote.single_instance()
+
+        def on_about_dialog_url(dialog, link):
+            import webbrowser
+            webbrowser.open(link)    
+        gtk.about_dialog_set_url_hook(on_about_dialog_url)
+        gtk.rc_parse_string('''
+style "hotwire-tab-close" {
+  xthickness = 0
+  ythickness = 0
+}
+widget "*hotwire-tab-close" style "hotwire-tab-close"
+''')    
+        if os.getenv('HOTWIRE_UNINSTALLED'):
+            theme = gtk.icon_theme_get_default()
+            imgpath = os.path.join(os.getenv('HOTWIRE_UNINSTALLED'), 'images')
+            _logger.debug("appending to icon theme: %s", imgpath)
+            theme.append_search_path(imgpath)    
+    
+        factory = app.get_factory()
+        factory.connect('shutdown', app.on_shutdown)
+        args = sys.argv[1:]
+        if len(args) == 0:
+            app.offer_load_session()
+        else:
+            w = factory.create_initial_window()
+            w.new_tab(args, os.getcwd())
+            w.show_all()
+            w.present()
+    
+        uiproxy = remote.get_proxy(factory)    
+
+        _logger.debug('entering mainloop')
+        gtk.gdk.threads_enter()
+        gtk.main()
+        gtk.gdk.threads_leave()    

Added: trunk/hotssh/sshwindow.py
==============================================================================
--- (empty file)
+++ trunk/hotssh/sshwindow.py	Tue May 20 15:11:46 2008
@@ -0,0 +1,1080 @@
+# This file is part of the Hotwire Shell user interface.
+#   
+# Copyright (C) 2007 Colin Walters <walters verbum org>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+import os,sys,platform,logging,getopt,re
+import locale,threading,subprocess,time
+import signal,tempfile,shutil,stat,pwd
+import datetime
+
+import xml.dom.minidom
+
+try:
+    import sqlite3
+except:
+    from pysqlite2 import dbapi2 as sqlite3
+
+import gtk,gobject,pango
+import dbus,dbus.glib,dbus.service
+
+try:
+    import avahi
+    avahi_available = True
+except:
+    avahi_available = False
+
+from hotssh.hotvte.vteterm import VteTerminalWidget
+from hotssh.hotvte.vtewindow import VteWindow
+from hotssh.hotvte.vtewindow import VteApp
+from hotssh.hotlib.logutil import log_except
+from hotssh.hotlib.timesince import timesince
+from hotssh.hotlib_ui.quickfind import QuickFindWindow
+from hotssh.hotlib_ui.msgarea import MsgAreaController
+
+_logger = logging.getLogger("hotssh.SshWindow")
+
+_whitespace_re = re.compile('\s+')
+
+class SshConnectionHistory(object):
+    def __init__(self):
+        self.__statedir = os.path.expanduser('~/.hotwire/state')
+        try:            
+            os.makedirs(self.__statedir)
+        except:
+            pass
+        self.__path = path =os.path.join(self.__statedir, 'ssh.sqlite')
+        self.__conn = sqlite3.connect(path, isolation_level=None)
+        cursor = self.__conn.cursor()
+        cursor.execute('''CREATE TABLE IF NOT EXISTS Connections (bid INTEGER PRIMARY KEY AUTOINCREMENT, host TEXT, user TEXT, options TEXT, conntime DATETIME)''')
+        cursor.execute('''CREATE INDEX IF NOT EXISTS ConnectionsIndex1 on Connections (host)''')
+        cursor.execute('''CREATE INDEX IF NOT EXISTS ConnectionsIndex2 on Connections (host,user)''')
+        cursor.execute('''CREATE TABLE IF NOT EXISTS Colors (bid INTEGER PRIMARY KEY AUTOINCREMENT, pattern TEXT UNIQUE, fg TEXT, bg TEXT, modtime DATETIME)''')
+        cursor.execute('''CREATE INDEX IF NOT EXISTS ColorsIndex on Colors (pattern)''')    
+
+    def get_recent_connections_search(self, host, limit=10):
+        cursor = self.__conn.cursor()
+        params = ()
+        q = '''SELECT user,host,options,conntime FROM Connections '''
+        if host:
+            q += '''WHERE host LIKE ? ESCAPE '%' '''
+            params = ('%' + host.replace('%', '%%') + '%',)
+        # We use a large limit here to avoid the query being totally unbounded
+        q += '''ORDER BY conntime DESC LIMIT 1000'''
+        def sqlite_timestamp_to_datetime(s):
+            return datetime.datetime.fromtimestamp(time.mktime(time.strptime(s[:-7], '%Y-%m-%d %H:%M:%S')))
+        # Uniquify, which we coudln't do in the SQL because we're also grabbing the conntime
+        seen = set()         
+        for user,host,options,conntime in cursor.execute(q, params):
+            if len(seen) >= limit:
+                break
+            if user:
+                uhost = user+'@'+host
+            else:
+                uhost = host
+            if uhost in seen:
+                continue
+            seen.add(uhost)
+            # We do this just to parse conntime
+            yield user,host,options,sqlite_timestamp_to_datetime(conntime)  
+
+    def get_users_for_host_search(self, host):
+        for user,host,options,ts in self.get_recent_connections_search(host):
+            yield user
+
+    def add_connection(self, host, user, options):
+        cursor = self.__conn.cursor()
+        cursor.execute('''BEGIN TRANSACTION''')
+        cursor.execute('''INSERT INTO Connections VALUES (NULL, ?, ?, ?, ?)''', (user, host, ' '.join(options), datetime.datetime.now()))
+        cursor.execute('''COMMIT''')
+
+    def get_color_for_host(self, host):
+        cursor = self.__conn.cursor()
+        res = cursor.execute('SELECT fg,bg FROM Colors WHERE pattern = ? ORDER BY conntime DESC LIMIT 10')
+        if len(res) > 0:
+            return res[0]
+        return None
+
+    def set_color_for_host(self, host, fg, bg):
+        cursor.execute('''BEGIN TRANSACTION''')
+        cursor.execute('''INSERT INTO Colors VALUES (NULL, ?, ?, ?, ?)''', (host, fg, bg, datetime.datetime.now()))
+        cursor.execute('''COMMIT''')
+_history_instance = None
+def get_history():
+    global _history_instance
+    if _history_instance is None:
+        _history_instance = SshConnectionHistory()
+    return _history_instance
+
+class OpenSSHKnownHostsDB(object):
+    def __init__(self):
+        super(OpenSSHKnownHostsDB, self).__init__()
+        self.__path = os.path.expanduser('~/.ssh/known_hosts')
+        self.__hostcache_ts_size = (None, None)
+        self.__hostcache = None
+        
+    def __read_hosts(self):
+        try:
+            _logger.debug("reading %s", self.__path)
+            f = open(self.__path)
+        except:
+            _logger.debug("failed to open known hosts")
+            return
+        hosts = set()
+        for line in f:
+            hostip,rest = line.split(' ', 1)
+            if hostip.find(',') > 0:
+                host = hostip.split(',', 1)[0]
+            else:
+                host = hostip
+            host = host.strip()
+            hosts.add(host)
+        f.close()
+        return hosts
+        
+    def get_hosts(self):
+        try:
+            stbuf = os.stat(self.__path)
+            ts_size =  (stbuf[stat.ST_MTIME], stbuf[stat.ST_SIZE])
+        except OSError, e:
+            ts_size = None
+        if ts_size is not None and self.__hostcache_ts_size != ts_size:
+            self.__hostcache = self.__read_hosts()
+            self.__hostcache_ts_size = ts_size
+        return self.__hostcache
+
+_openssh_hosts_db = OpenSSHKnownHostsDB()
+
+class SshAvahiMonitor(gobject.GObject):
+    __gsignals__ = {
+        "changed" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, []),
+    }
+    def __init__(self):
+        super(SshAvahiMonitor, self).__init__()
+        # maps name->(addr,port)
+        self.__cache = {}
+        
+        if not avahi_available:
+            return
+        
+        sysbus = dbus.SystemBus()
+        avahi_service = sysbus.get_object(avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER)
+        self.__avahi = dbus.Interface(avahi_service, avahi.DBUS_INTERFACE_SERVER)
+
+        browser_ref = self.__avahi.ServiceBrowserNew(avahi.IF_UNSPEC,
+                                                     avahi.PROTO_UNSPEC,
+                                                     '_ssh._tcp', 'local',
+                                                     dbus.UInt32(0))
+        browser = sysbus.get_object(avahi.DBUS_NAME, browser_ref)
+        self.__browser = dbus.Interface(browser, avahi.DBUS_INTERFACE_SERVICE_BROWSER)
+        self.__browser.connect_to_signal('ItemNew', self.__on_item_new)
+        self.__browser.connect_to_signal('ItemRemove', self.__on_item_remove)
+
+    def __iter__(self):
+        for k,addr,port in self.__cache.iteritems():
+            yield (k, addr, port)
+
+    def __on_resolve_reply(self, *args):
+        addr, port = args[-4:-2]
+        name = args[2].decode('utf-8', 'replace')
+        _logger.debug("add Avahi SSH: %r %r %r", name, addr, port)
+        self.__cache[name] = (addr, port)
+        self.emit('changed')
+        
+    def __on_avahi_error(self, *args):
+        _logger.debug("Avahi error: %s", args)
+        
+    def __on_item_new(self, interface, protocol, name, type, domain, flags):
+        self.__avahi.ResolveService(interface, protocol, name, type, domain,
+                                    avahi.PROTO_UNSPEC, dbus.UInt32(0),
+                                    reply_handler=self.__on_resolve_reply,
+                                    error_handler=self.__on_avahi_error)
+
+    def __on_item_remove(self, interface, protocol, name, type, domain,server):
+        uname = name.decode('utf-8', 'replace')
+        if uname in self.__cache:
+            _logger.debug("del Avahi SSH: %r", uname)            
+            del self.__cache[uname]
+            self.emit('changed')
+            
+_local_avahi_instance = None
+def get_local_avahi():
+    global _local_avahi_instance
+    if _local_avahi_instance is None:
+        _local_avahi_instance = SshAvahiMonitor()
+    return _local_avahi_instance            
+            
+class SshOptions(gtk.Expander):
+    def __init__(self):
+        super(SshOptions, self).__init__(_('Options'))
+        self.set_label('<b>%s</b>' % (gobject.markup_escape_text(self.get_label())))
+        self.set_use_markup(True)
+        options_vbox = gtk.VBox()
+        options_hbox = gtk.HBox()
+        options_vbox.add(options_hbox)
+        self.__entry = gtk.Entry()
+        options_hbox.pack_start(self.__entry, expand=True)
+        options_help = gtk.Button(stock=gtk.STOCK_HELP)
+        options_help.connect('clicked', self.__on_options_help_clicked)
+        options_hbox.pack_start(options_help, expand=False)
+        options_helplabel = gtk.Label(_('Example: '))
+        options_helplabel.set_markup('<i>%s -C -Y -oTCPKeepAlive=false</i>' % (gobject.markup_escape_text(options_helplabel.get_label()),))
+        options_helplabel.set_alignment(0.0, 0.5)
+        options_vbox.add(options_helplabel)
+        self.add(options_vbox)
+        
+    def get_entry(self):
+        return self.__entry   
+    
+    def __on_options_help_clicked(self, b):
+        # Hooray Unix!
+        subprocess.Popen(['gnome-terminal', '-x', 'man', 'ssh'])
+        
+class LocalConnectDialog(gtk.Dialog):
+    def __init__(self, parent=None, local_avahi=None):    
+        super(LocalConnectDialog, self).__init__(title=_("New Local Secure Shell Connection"),
+                                            parent=parent,
+                                            flags=gtk.DIALOG_DESTROY_WITH_PARENT,
+                                            buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
+        self.connect('response', lambda *args: self.hide())
+        self.connect('delete-event', self.hide_on_delete)
+        button = self.add_button(_('C_onnect'), gtk.RESPONSE_ACCEPT)
+        button.set_property('image', gtk.image_new_from_stock('gtk-connect', gtk.ICON_SIZE_BUTTON))
+        self.set_default_response(gtk.RESPONSE_ACCEPT)
+                
+        self.set_has_separator(False)
+        self.set_border_width(5)
+        self.__vbox = vbox = gtk.VBox()
+        self.vbox.add(self.__vbox)   
+        self.vbox.set_spacing(6)
+        
+        frame = gtk.Frame()
+        #frame.set_label_widget(history_label)
+        self.__model = gtk.ListStore(str, str, int)
+        self.__view = gtk.TreeView(self.__model)
+        self.__view.connect('row-activated', self.__on_item_activated)
+        frame.add(self.__view)
+        colidx = self.__recent_view.insert_column_with_attributes(-1, _('Name'),
+                                                          gtk.CellRendererText(),
+                                                          'text', 0)
+        colidx = self.__recent_view.insert_column_with_attributes(-1, _('Address'),
+                                                          gtk.CellRendererText(),
+                                                          'text', 1)
+        vbox.pack_start(frame, expand=True)
+        self.__reload_avahi()
+        
+        self.__options_expander = SshOptions()
+        self.__options_entry = self.__options_expander.get_entry()           
+        
+        vbox.pack_start(gtk.Label(' '), expand=False)
+        hbox = gtk.HBox()
+        vbox.pack_start(hbox, expand=False)
+        user_label = gtk.Label(_('User: '))
+        user_label.set_markup('<b>%s</b>' % (gobject.markup_escape_text(user_label.get_text())))
+        sg.add_widget(user_label)
+        hbox.pack_start(user_label, expand=False)
+        self.__user_entry = gtk.Entry()
+        self.__set_user(None)
+        
+        hbox.pack_start(self.__user_entry, expand=False) 
+  
+        vbox.pack_start(self.__options_expander, expand=False)        
+
+        self.set_default_size(640, 480)
+        
+    def __set_user(self, name):
+        if name is None:
+            name = pwd.getpwuid(os.getuid()).pw_name
+        self.__user_entry.set_text(name)    
+        
+    def __on_item_activated(self, tv, path, vc):
+        self.activate_default()
+            
+    def run_get_cmd(self):
+        self.show_all()        
+        resp = self.run()
+        if resp != gtk.RESPONSE_ACCEPT:
+            return None
+        host = self.__view.get_selection().get_selected()
+        if not host:
+            return None
+        args = [x for x in _whitespace_re.split(self.__options_entry.get_text()) if x != '']
+        if self.__custom_user:
+            args.append(self.__user_entry.get_text() + '@' + host)
+        else:
+            args.append(host)
+        return args                    
+
+class ConnectDialog(gtk.Dialog):
+    def __init__(self, parent=None, history=None):
+        super(ConnectDialog, self).__init__(title=_("New Secure Shell Connection"),
+                                            parent=parent,
+                                            flags=gtk.DIALOG_DESTROY_WITH_PARENT,
+                                            buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
+        
+        self.__history = history
+
+        self.connect('response', lambda *args: self.hide())
+        self.connect('delete-event', self.hide_on_delete)
+        button = self.add_button(_('C_onnect'), gtk.RESPONSE_ACCEPT)
+        button.set_property('image', gtk.image_new_from_stock('gtk-connect', gtk.ICON_SIZE_BUTTON))
+        self.set_default_response(gtk.RESPONSE_ACCEPT)
+                
+        self.set_has_separator(False)
+        self.set_border_width(5)
+        
+        self.__vbox = vbox =gtk.VBox()
+        self.vbox.add(self.__vbox)   
+        self.vbox.set_spacing(6)
+
+        self.__response_value = None
+   
+        self.__suppress_recent_search = False
+        self.__idle_search_id = 0
+        self.__idle_update_search_id = 0
+        
+        self.__custom_user = False
+   
+        self.__hosts = _openssh_hosts_db
+
+        sg = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
+        
+        hbox = gtk.HBox()
+        vbox.pack_start(hbox, expand=False)
+        host_label = gtk.Label(_('Host: '))
+        host_label.set_markup('<b>%s</b>' % (gobject.markup_escape_text(host_label.get_text())))
+        sg.add_widget(host_label)
+        hbox.pack_start(host_label, expand=False)
+        self.__entry = gtk.combo_box_entry_new_text()
+        self.__entry.child.connect('activate', self.__on_entry_activated)
+        self.__entry.child.connect('notify::text', self.__on_entry_modified)
+        self.__entrycompletion = gtk.EntryCompletion()
+        self.__entrycompletion.set_property('inline-completion', True)
+        self.__entrycompletion.set_property('popup-completion', False)
+        self.__entrycompletion.set_property('popup-single-match', False)
+        self.__entrycompletion.set_model(self.__entry.get_property('model'))
+        self.__entrycompletion.set_text_column(0)     
+        self.__entry.child.set_completion(self.__entrycompletion)
+        hbox.add(self.__entry)
+        self.__reload_entry()
+
+        hbox = gtk.HBox()
+        vbox.pack_start(hbox, expand=False)
+        user_label = gtk.Label(_('User: '))
+        user_label.set_markup('<b>%s</b>' % (gobject.markup_escape_text(user_label.get_text())))
+        sg.add_widget(user_label)
+        hbox.pack_start(user_label, expand=False)
+        self.__user_entry = gtk.Entry()
+        self.__set_user(None)
+        self.__user_entry.connect('notify::text', self.__on_user_modified)
+        self.__user_entry.set_activates_default(True)
+        self.__user_completion = gtk.EntryCompletion()
+        self.__user_completion.set_property('inline-completion', True)
+        self.__user_completion.set_property('popup-single-match', False)
+        self.__user_completion.set_model(gtk.ListStore(gobject.TYPE_STRING))
+        self.__user_completion.set_text_column(0)     
+        self.__user_entry.set_completion(self.__user_completion)
+
+        hbox.pack_start(self.__user_entry, expand=False)
+
+        vbox.pack_start(gtk.Label(' '), expand=False)
+
+        history_label = gtk.Label(_('Connection History'))
+        history_label.set_markup('<b>%s</b>' % (gobject.markup_escape_text(history_label.get_text())))        
+        frame = gtk.Frame()
+        #frame.set_label_widget(history_label)
+        self.__recent_model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_STRING, gobject.TYPE_PYOBJECT)
+        self.__recent_view = gtk.TreeView(self.__recent_model)
+        self.__recent_view.get_selection().connect('changed', self.__on_recent_selected)
+        self.__recent_view.connect('row-activated', self.__on_recent_activated)
+        frame.add(self.__recent_view)
+        colidx = self.__recent_view.insert_column_with_data_func(-1, _('Connection'),
+                                                          gtk.CellRendererText(),
+                                                          self.__render_userhost)
+        colidx = self.__recent_view.insert_column_with_data_func(-1, _('Time'),
+                                                          gtk.CellRendererText(),
+                                                          self.__render_time_recency, datetime.datetime.now())
+        vbox.pack_start(frame, expand=True)
+        self.__on_entry_modified()
+        self.__reload_connection_history()
+        
+        self.__options_expander = SshOptions()
+        self.__options_entry = self.__options_expander.get_entry()
+
+        vbox.pack_start(self.__options_expander, expand=False)        
+
+        self.set_default_size(640, 480)
+        
+    def __on_browse_local(self, b):
+        pass
+
+    def __render_userhost(self, col, cell, model, iter):
+        user = model.get_value(iter, 0)
+        host = model.get_value(iter, 1)
+        if user:
+            userhost = user + '@' + host
+        else:
+            userhost = host
+        cell.set_property('text', userhost)
+        
+    def __set_user(self, name):
+        if name is None:
+            name = pwd.getpwuid(os.getuid()).pw_name
+        self.__user_entry.set_text(name) 
+        
+    def __render_time_recency(self, col, cell, model, iter, curtime):
+        val = model.get_value(iter, 2)
+        deltastr = timesince(val, curtime)
+        cell.set_property('text', deltastr)
+       
+    def __reload_entry(self, *args, **kwargs):
+        _logger.debug("reloading")
+        # TODO do predictive completion here
+        # For example, I have in my history:
+        # foo.cis.ohio-state.edu
+        # bar.cis.ohio-state.edu
+        # Now I type baz.cis
+        # The system notices that nothing matches baz.cis; however
+        # the "cis" part does match foo.cis.ohio-state.edu, so 
+        # we start offering a completion for it
+        self.__entry.get_property('model').clear()
+        for host in self.__hosts.get_hosts():
+            self.__entry.append_text(host)
+            
+    def __on_user_modified(self, *args):
+        if self.__suppress_recent_search:
+            return        
+        self.__custom_user = True
+
+    def __on_entry_modified(self, *args):
+        text = self.__entry.get_active_text()
+        havetext = text != ''
+        self.set_response_sensitive(gtk.RESPONSE_ACCEPT, havetext)
+        if self.__suppress_recent_search:
+            return        
+        if havetext and self.__idle_update_search_id == 0:
+            self.__idle_update_search_id = gobject.timeout_add(250, self.__idle_update_search)
+
+    def __force_idle_search(self):
+        if self.__idle_update_search_id > 0:
+            gobject.source_remove(self.__idle_update_search_id)
+        self.__idle_update_search()
+
+    def __idle_update_search(self):
+        self.__idle_update_search_id = 0
+        host = self.__entry.get_active_text()
+        usernames = list(self.__history.get_users_for_host_search(host))
+        if len(usernames) > 0:
+            last_user = usernames[0]
+            if last_user:
+                self.__user_entry.set_text(usernames[0])
+            model = gtk.ListStore(gobject.TYPE_STRING)
+            for uname in usernames:
+                if uname:
+                    model.append((uname,))
+            self.__user_completion.set_model(model)
+        self.__reload_connection_history()            
+
+    def __reload_connection_history(self):
+        hosttext = self.__entry.get_active_text()
+        self.__recent_model.clear()
+        for user,host,options,ts in self.__history.get_recent_connections_search(hosttext):
+            self.__recent_model.append((user, host, ts))
+
+    def __on_entry_activated(self, *args):
+        self.__force_idle_search()
+        hosttext = self.__entry.get_active_text()
+        if hosttext.find('@') >= 0:
+            (user, host) = hosttext.split('@', 1)
+            self.__user_entry.set_text(user)
+            self.__entry.child.set_text(host)
+            self.__user_entry.activate()
+        else:
+            self.__user_entry.select_region(0, -1)
+            self.__user_entry.grab_focus()
+            
+    def __on_recent_selected(self, ts):
+        (tm, seliter) = ts.get_selected()
+        if seliter is None: 
+            return
+        user = self.__recent_model.get_value(seliter, 0)
+        host = self.__recent_model.get_value(seliter, 1)
+        self.__suppress_recent_search = True
+        self.__entry.child.set_text(host)
+        if user:
+            self.__user_entry.set_text(user)
+        else:
+            self.__set_user(None)
+        self.__suppress_recent_search = False        
+            
+    def __on_recent_activated(self, tv, path, vc):
+        self.activate_default()
+            
+    def run_get_cmd(self):
+        self.show_all()        
+        resp = self.run()
+        if resp != gtk.RESPONSE_ACCEPT:
+            return None
+        host = self.__entry.get_active_text()
+        if not host:
+            return None
+        args = [x for x in _whitespace_re.split(self.__options_entry.get_text()) if x != '']
+        if self.__custom_user:
+            args.append(self.__user_entry.get_text() + '@' + host)
+        else:
+            args.append(host)
+        return args
+
+_CONTROLPATH = None
+def get_controlpath(create=True):
+    global _CONTROLPATH
+    if _CONTROLPATH is None and create:
+        _CONTROLPATH = tempfile.mkdtemp('', 'hotssh')
+    return _CONTROLPATH
+
+def get_base_sshcmd():
+    return ['ssh']
+
+def get_connection_sharing_args():
+    # TODO - openssh should really do this out of the box    
+    return ['-oControlMaster=auto', '-oControlPath=' + os.path.join(get_controlpath(), 'master-%r %h:%p')]
+
+class HostConnectionMonitor(gobject.GObject):
+    __gsignals__ = {
+        "host-status" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,gobject.TYPE_BOOLEAN,gobject.TYPE_PYOBJECT)),
+    }      
+    def __init__(self):
+        super(HostConnectionMonitor, self).__init__()
+        self.__host_monitor_ids = {}
+        self.__check_statuses = {}
+    
+    def start_monitor(self, host):
+        if not (host in self.__host_monitor_ids or host in self.__check_statuses):
+            _logger.debug("adding monitor for %s", host)            
+            self.__host_monitor_ids[host] = gobject.timeout_add(3000, self.__check_host, host)
+            
+    def stop_monitor(self, host):
+        _logger.debug("stopping monitor for %s", host)
+        if host in self.__host_monitor_ids:
+            monid = self.__host_monitor_ids[host]
+            gobject.source_remove(monid)
+            del self.__host_monitor_ids[host]
+        if host in self.__check_statuses:
+            del self.__check_statuses[host]
+        
+    def get_monitors(self):
+        return self.__host_monitor_ids
+            
+    def __check_host(self, host):
+        _logger.debug("performing check for %s", host)
+        del self.__host_monitor_ids[host]
+        cmd = list(get_base_sshcmd())
+        starttime = time.time()
+        # This is a hack.  Blame Adam Jackson.
+        cmd.extend(['-oBatchMode=true', host, '/bin/true'])
+        subproc = subprocess.Popen(cmd)
+        child_watch_id = gobject.child_watch_add(subproc.pid, self.__on_check_exited, host)
+        timeout_id = gobject.timeout_add(7000, self.__check_timeout, host)
+        self.__check_statuses[host] = (starttime, subproc.pid, timeout_id, child_watch_id)
+        return False
+        
+    def __check_timeout(self, host):
+        _logger.debug("timeout for host=%s", host)
+        try:
+            (starttime, pid, timeout_id, child_watch_id) = self.__check_statuses[host]
+        except KeyError, e:
+            return False
+        try:
+            os.kill(pid, signal.SIGHUP)
+        except OSError, e:
+            _logger.debug("failed to signal pid %s", pid, exc_info=True)
+            pass
+        return False    
+        
+    def __on_check_exited(self, pid, condition, host):
+        _logger.debug("check exited, pid=%s condition=%s host=%s", pid, condition, host)
+        try:
+            (starttime, pid, timeout_id, child_watch_id) = self.__check_statuses[host]
+        except KeyError, e:
+            return False
+        gobject.source_remove(timeout_id)
+        del self.__check_statuses[host]    
+        self.__host_monitor_ids[host] = gobject.timeout_add(4000, self.__check_host, host)              
+        self.emit('host-status', host, condition == 0, time.time()-starttime)
+        return False
+        
+_hostmonitor = HostConnectionMonitor()
+
+class SshTerminalWidgetImpl(VteTerminalWidget):
+    def __init__(self, *args, **kwargs):
+        self.__actions = kwargs['actions']
+        super(SshTerminalWidgetImpl, self).__init__(*args, **kwargs)
+
+    def _get_extra_context_menuitems(self):
+        return [self.__actions.get_action(x).create_menu_item() for x in ['CopyConnection', 'OpenSFTP']]
+
+class SshTerminalWidget(gtk.VBox):
+    __gsignals__ = {
+        "close" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, []),
+    }       
+    def __init__(self, args, cwd, actions=None):
+        super(SshTerminalWidget, self).__init__()
+        self.__init_state()
+        self.__args = args        
+        self.__sshcmd = list(get_base_sshcmd())
+        self.__sshcmd.extend(args)
+        self.__cwd = cwd
+        self.__host = None
+        self.__sshopts = []
+        self.__actions = actions
+        enable_connection_sharing = True
+        for arg in args:
+            if not arg.startswith('-'):
+                if self.__host is None:                 
+                    self.__host = arg
+            else:
+                if arg == '-oControlPath=none':
+                    enable_connection_sharing = False
+                self.__sshopts.append(arg)
+        if enable_connection_sharing:
+            self.__sshcmd.extend(get_connection_sharing_args())
+        
+        header = gtk.VBox()
+        self.__msg = gtk.Label()
+        self.__msg.set_alignment(0.0, 0.5)
+        self.__msgarea_mgr = MsgAreaController()
+        header.pack_start(self.__msg)
+        header.pack_start(self.__msgarea_mgr)
+        self.pack_start(header, expand=False)
+        self.ssh_connect()
+        
+    def __init_state(self):
+        self.__connecting_state = False
+        self.__connected = None
+        self.__cmd_exited = False
+        self.__latency = None        
+        
+    def set_status(self, connected, latency):
+        if not connected and self.__connecting_state:
+            return
+        self.__connecting_state = False
+        connected_changed = self.__connected != connected
+        latency_changed = (not self.__latency) or (self.__latency*0.9 > latency) or (self.__latency*1.1 < latency)
+        if not (connected_changed or latency_changed):
+            return        
+        self.__connected = connected
+        self.__latency = latency
+        self.__sync_msg()
+        
+    def __sync_msg(self):
+        if self.__cmd_exited:
+            return
+        if self.__connecting_state:
+            text = _('Connecting')
+        elif self.__connected is True:
+            text = _('Connected (%.2fs latency)') % (self.__latency)
+        elif self.__connected is False:
+            text = '<span foreground="red">%s</span>' % (_('Connection timeout'))
+        elif self.__connected is None:
+            text = _('Checking connection')
+        if len(self.__sshopts) > 1:
+            text += _('; Options: ') + (' '.join(map(gobject.markup_escape_text, self.__sshopts)))
+        self.__msg.show()
+        self.__msg.set_markup(text)
+        
+    def ssh_connect(self):
+        self.__connecting_state = True        
+        self.__term = term = SshTerminalWidgetImpl(cwd=self.__cwd, cmd=self.__sshcmd, actions=self.__actions)
+        term.connect('child-exited', self.__on_child_exited)
+        term.show_all()
+        self.pack_start(term, expand=True)
+        # For some reason, VTE doesn't have the CAN_FOCUS flag set, so we can't call
+        # grab_focus.  Do it manually then:
+        term.emit('focus', True)
+        self.__msgarea_mgr.clear()
+        self.__sync_msg()
+        
+    def ssh_reconnect(self):
+        # TODO - do this in a better way
+        if not self.__term.exited:
+            os.kill(self.__term.pid, signal.SIGTERM)
+        self.remove(self.__term)
+        self.__term.destroy()
+        self.__init_state()
+        self.ssh_connect()
+        
+    def __on_child_exited(self, term):
+        _logger.debug("disconnected")
+        self.__cmd_exited = True
+        self.__msg.hide()
+        msgarea = self.__msgarea_mgr.new_from_text_and_icon(gtk.STOCK_INFO, _('Connection closed'), 
+                                                            buttons=[(gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE)])
+        reconnect = self.__actions.get_action('Reconnect')
+        msgarea.add_stock_button_with_text(reconnect.get_property('label'), 
+                                           reconnect.get_property('stock-id'), gtk.RESPONSE_ACCEPT)
+        msgarea.connect('response', self.__on_msgarea_response)
+        msgarea.show_all()
+        
+    def __on_msgarea_response(self, msgarea, respid):
+        if respid == gtk.RESPONSE_ACCEPT:
+            self.ssh_reconnect()
+        else:
+            self.emit('close')
+
+    def has_close(self):
+        return True
+        
+    def get_exited(self):
+        return self.__cmd_exited        
+        
+    def get_term(self):
+        return self.__term
+        
+    def get_vte(self):
+        return self.__term.get_vte()
+        
+    def get_title(self):
+        return self.get_host()
+    
+    def get_host(self):
+        return self.__host
+    
+    def get_options(self):
+        return self.__sshopts
+
+class SshWindow(VteWindow):
+    def __init__(self, **kwargs):
+        super(SshWindow, self).__init__(title='HotSSH', icon_name='hotwire-openssh', **kwargs)
+        
+        self.__ui_string = """
+<ui>
+  <menubar name='Menubar'>
+    <menu action='FileMenu'>
+      <placeholder name='FileAdditions'>
+        <menuitem action='NewConnection'/>
+        <menuitem action='NewLocalConnection'/>        
+        <menuitem action='CopyConnection'/>    
+        <menuitem action='OpenSFTP'/>
+        <separator/>
+        <menuitem action='Reconnect'/>
+        <separator/>
+      </placeholder>
+    </menu>
+  </menubar>
+</ui>
+"""       
+
+        self._get_notebook().connect('switch-page', self.__on_page_switch)
+
+        try:
+            self.__nm_proxy = dbus.SystemBus().get_object('org.freedesktop.NetworkManager', '/org/freedesktop/NetworkManager')
+            self.__nm_proxy.connect_to_signal('StateChange', self.__on_nm_state_change)
+        except dbus.DBusException, e:
+            _logger.debug("Couldn't find NetworkManager")
+            self.__nm_proxy = None
+        
+        self.__idle_stop_monitoring_id = 0
+        
+        self.connect("notify::is-active", self.__on_is_active_changed)
+        _hostmonitor.connect('host-status', self.__on_host_status)
+
+        self.__connhistory = get_history()
+        self.__local_avahi = get_local_avahi()
+        
+        self.__merge_ssh_ui()
+
+    def __add_to_history(self, args):
+        user = None
+        host = None
+        options = []
+        for arg in args:
+            if arg.startswith('-'): 
+                options.extend(arg)
+                continue
+            if arg.find('@') >= 0:
+                (user, host) = arg.split('@', 1)
+            else:
+                host = arg
+        self.__connhistory.add_connection(user, host, options)
+
+    def new_tab(self, args, cwd):
+        if len(args) == 0:
+            self.open_connection_dialog(exit_on_cancel=True)
+        else:
+            self.__add_to_history(args)
+            term = SshTerminalWidget(args=args, cwd=cwd, actions=self.__action_group)
+            self.append_widget(term)
+        
+    def __on_nm_state_change(self, *args):
+        self.__sync_nm_state()
+        
+    def __sync_nm_state(self):
+        self.__nm_proxy.GetActiveConnections(reply_handler=self.__on_nm_connections, error_handler=self.__on_dbus_error)
+        
+    def __on_dbus_error(self, *args):
+        _logger.debug("caught DBus error: %r", args, exc_info=True)
+        
+    @log_except(_logger)        
+    def __on_nm_connections(self, connections):
+        _logger.debug("nm connections: %s", connections)    
+        
+    @log_except(_logger)        
+    def __on_host_status(self, hostmon, host, connected, latency):
+        _logger.debug("got host status host=%s conn=%s latency=%s", host, connected, latency)
+        for widget in self._get_notebook().get_children():
+            child_host = widget.get_host()
+            if child_host != host:
+                continue
+            widget.set_status(connected, latency)
+            
+    @log_except(_logger)            
+    def __on_is_active_changed(self, *args):
+        isactive = self.get_property('is-active')
+        if isactive:
+            self.__start_monitoring()
+            if self.__idle_stop_monitoring_id > 0:
+                gobject.source_remove(self.__idle_stop_monitoring_id)
+                self.__idle_stop_monitoring_id = 0
+        elif self.__idle_stop_monitoring_id == 0:
+            self.__idle_stop_monitoring_id = gobject.timeout_add(8000, self.__idle_stop_monitoring)
+            
+    def __idle_stop_monitoring(self):
+        self.__idle_stop_monitoring_id = 0
+        self.__stop_monitoring()
+        
+    @log_except(_logger)
+    def __on_page_switch(self, n, p, pn):
+        # Becuase of the way get_current_page() works in this signal handler, this
+        # will actually disable monitoring for the previous tab, and enable it
+        # for the new current one.
+        self.__stop_monitoring()
+        self.__start_monitoring(pn=pn)
+            
+    def __stop_monitoring(self):
+        notebook = self._get_notebook()
+        pn = notebook.get_current_page()
+        if pn >= 0:
+            prev_widget = notebook.get_nth_page(pn)
+            prev_host = prev_widget.get_host()
+            _hostmonitor.stop_monitor(prev_host)
+            prev_widget.set_status(None, None)        
+            
+    def __start_monitoring(self, pn=None):
+        notebook = self._get_notebook()
+        if pn is not None:
+            pagenum = pn
+        else:
+            pagenum = notebook.get_current_page()
+        widget = notebook.get_nth_page(pagenum)
+        _hostmonitor.start_monitor(widget.get_host())
+        
+    def __merge_ssh_ui(self):
+        self.__using_accels = True
+        self.__actions = actions = [
+            ('NewConnection', gtk.STOCK_NEW, _('Connect to server'), '<control><shift>O',
+             _('Open a new Secure Shell connection'), self.__new_connection_cb),
+            ('NewLocalConnection', gtk.STOCK_NEW, _('Connect to local server'), None,
+             _('Open a new Secure Shell connection to local server'), self.__new_local_connection_cb),                
+            ('CopyConnection', gtk.STOCK_JUMP_TO, _('New tab for connection'), '<control><shift>T',
+             _('Open a new tab for the same remote computer'), self.__copy_connection_cb),              
+            ('OpenSFTP', gtk.STOCK_OPEN, _('Open SFTP'), '<control><shift>S',
+             _('Open a SFTP connection'), self.__open_sftp_cb),            
+            ('ConnectionMenu', None, _('Connection')),
+            ('Reconnect', gtk.STOCK_CONNECT, _('_Reconnect'), '<control><shift>R', _('Reset connection to server'), self.__reconnect_cb),
+            ]
+        self.__action_group = self._merge_ui(self.__actions, self.__ui_string)
+        
+    @log_except(_logger)        
+    def __copy_connection_cb(self, action):
+        notebook = self._get_notebook()
+        widget = notebook.get_nth_page(notebook.get_current_page())
+        host = widget.get_host()
+        opts = widget.get_options()
+        args = list(opts)
+        args.append(host)
+        self.new_tab(args, None)
+
+    def open_connection_dialog(self, exit_on_cancel=False):
+        win = ConnectDialog(parent=self, history=self.__connhistory)
+        sshargs = win.run_get_cmd()
+        if not sshargs:
+            # We get here when called with no arguments, and we're the main instance.
+            if exit_on_cancel:
+                sys.exit(0)
+            return
+        self.new_tab(sshargs, None)
+
+    @log_except(_logger)        
+    def __new_connection_cb(self, action):
+        self.open_connection_dialog()
+        
+    @log_except(_logger)        
+    def __new_local_connection_cb(self, action):
+        win = LocalConnectDialog(parent=self, local_avahi=self.__local_avahi)
+        sshargs = win.run_get_cmd()
+        if not sshargs:
+            return
+        self.new_tab(sshargs, None)  
+        
+    @log_except(_logger)        
+    def __open_sftp_cb(self, action):
+        notebook = self._get_notebook()        
+        widget = notebook.get_nth_page(notebook.get_current_page())
+        host = widget.get_host()
+        subprocess.Popen(['nautilus', 'sftp://%s' % (host,)])
+        
+    @log_except(_logger)        
+    def __reconnect_cb(self, a):
+        notebook = self._get_notebook()        
+        widget = notebook.get_nth_page(notebook.get_current_page())
+        widget.ssh_reconnect()
+
+class SshApp(VteApp):
+    def __init__(self):
+        super(SshApp, self).__init__(SshWindow)
+        self.__sessionpath = os.path.expanduser('~/.hotwire/hotwire-ssh.session')
+        self.__connhistory = get_history()
+        self.__local_avahi = SshAvahiMonitor() 
+        
+    @staticmethod
+    def get_name():
+        return 'HotSSH'         
+                
+    def on_shutdown(self, factory):
+        super(SshApp, self).on_shutdown(factory)
+        cp = get_controlpath(create=False)
+        if cp is not None:
+            try:
+                _logger.debug("removing %s", cp)
+                shutil.rmtree(cp)
+            except:
+                pass
+            
+    def offer_load_session(self):
+        savedsession = self._parse_saved_session()
+        allhosts = set()        
+        if savedsession:
+            for window in savedsession:
+                for connection in window:
+                    allhosts.add(connection['userhost'])
+        if len(allhosts) > 0:
+            dlg = gtk.MessageDialog(parent=None, flags=0, type=gtk.MESSAGE_QUESTION, 
+                                    buttons=gtk.BUTTONS_CANCEL,
+                                    message_format=_("Restore saved session?"))
+            button = dlg.add_button(_('_Reconnect'), gtk.RESPONSE_ACCEPT)
+            button.set_property('image', gtk.image_new_from_stock('gtk-connect', gtk.ICON_SIZE_BUTTON))
+            dlg.set_default_response(gtk.RESPONSE_ACCEPT)
+            dlg.format_secondary_markup(_('Reconnect to %d hosts') % (len(allhosts),))
+            
+            #ls = gtk.ListStore(str)
+            #gv = gtk.TreeView(ls)
+            #colidx = gv.insert_column_with_attributes(-1, _('Connection'),
+            #                                          gtk.CellRendererText(),
+            #                                          text=0)
+            #for host in allhosts:
+            #    ls.append((host,))
+            #dlg.add(gv)            
+                     
+            resp = dlg.run()
+            dlg.destroy()
+            if resp == gtk.RESPONSE_ACCEPT:
+                self._load_session(savedsession)
+                return
+        w = self.get_factory().create_initial_window()
+        w.new_tab([], os.getcwd())
+        w.show_all()
+        w.present()
+        
+    def _load_session(self, session):
+        factory = self.get_factory()        
+        for window in session:
+            window_impl = factory.create_window()
+            for connection in window:
+                args = [connection['userhost']]
+                if 'options' in connection:
+                    args.extend(connection['options'])
+                widget = window_impl.new_tab(args, os.getcwd())
+            window_impl.show_all()
+                
+    #override
+    @log_except(_logger)
+    def _parse_saved_session(self):
+        factory = self.get_factory()
+        try:
+            f = open(self.__sessionpath)
+        except:
+            return None
+        doc = xml.dom.minidom.parse(f)
+        saved_windows = []
+        current_widget = None
+        for window_child in doc.documentElement.childNodes:
+            if not (window_child.nodeType == window_child.ELEMENT_NODE and window_child.nodeName == 'window'): 
+                continue
+            connections = []
+            for child in window_child.childNodes:
+                if not (child.nodeType == child.ELEMENT_NODE and child.nodeName == 'connection'): 
+                    continue
+                host = child.getAttribute('host')
+                iscurrent = child.getAttribute('current')
+                options = []
+                for options_elt in child.childNodes:
+                    if not (options_elt.nodeType == child.ELEMENT_NODE and options_elt.nodeName == 'options'): 
+                        continue
+                    for option_elt in child.childNodes:
+                        if not (option_elt.nodeType == child.ELEMENT_NODE and options_elt.nodeName == 'option'): 
+                            continue
+                        options.append(option.firstChild.nodeValue)
+                kwargs = {'userhost': host}
+                if len(options) > 0:
+                    kwargs['options'] = options
+                if iscurrent:
+                    kwargs['current'] = True
+                connections.append(kwargs)
+            saved_windows.append(connections)
+        return saved_windows
+        
+    #override
+    @log_except(_logger)    
+    def save_session(self):
+        _logger.debug("doing session save")
+        tempf_path = tempfile.mktemp('.session.tmp', 'hotwire-session', os.path.dirname(self.__sessionpath))
+        f = open(tempf_path, 'w')
+        state = []
+        doc = xml.dom.minidom.getDOMImplementation().createDocument(None, "session", None)
+        root = doc.documentElement
+        factory = self.get_factory()
+        for window in factory._get_windows():
+            notebook = window._get_notebook()
+            current = notebook.get_nth_page(notebook.get_current_page())
+            window_node = doc.createElement('window')
+            root.appendChild(window_node)            
+            for widget in notebook:
+                connection = doc.createElement('connection')
+                window_node.appendChild(connection)
+                connection.setAttribute('host', widget.get_host())
+                if current == widget:
+                    connection.setAttribute('current', 'true')
+                options = widget.get_options()
+                if options:
+                    options_elt = doc.createElement('options')
+                    connection.appendChild(options_elt)
+                    for option in options:
+                        opt = doc.createElement('option')
+                        options_elt.appendChild(opt)                    
+                        opt.appendChild(doc.createTextNode(option))
+        f.write(doc.toprettyxml())
+        f.close()
+        os.rename(tempf_path, self.__sessionpath)            

Added: trunk/hotssh/version.py
==============================================================================
--- (empty file)
+++ trunk/hotssh/version.py	Tue May 20 15:11:46 2008
@@ -0,0 +1,30 @@
+# This file is part of the Hotwire Shell project API.
+
+# Copyright (C) 2007 Colin Walters <walters verbum org>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy 
+# of this software and associated documentation files (the "Software"), to deal 
+# in the Software without restriction, including without limitation the rights 
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 
+# of the Software, and to permit persons to whom the Software is furnished to do so, 
+# subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in all 
+# copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 
+# PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE 
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
+# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR 
+# THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+__version__ = '0.201'
+
+def svn_version_str():
+    if not svn_version_info:
+        return ''
+    return '(%s  %s)' % (svn_version_info['Revision'], svn_version_info['Last Changed Date'])
+
+# Default to empty
+svn_version_info = None

Added: trunk/hotwire-ssh.csh
==============================================================================
--- (empty file)
+++ trunk/hotwire-ssh.csh	Tue May 20 15:11:46 2008
@@ -0,0 +1,2 @@
+[ -x //usr/bin/id ] || exit
+[ `//usr/bin/id -u` -gt 100 ] && alias ssh hotwire-ssh --bg

Added: trunk/hotwire-ssh.desktop
==============================================================================
--- (empty file)
+++ trunk/hotwire-ssh.desktop	Tue May 20 15:11:46 2008
@@ -0,0 +1,11 @@
+[Desktop Entry]
+Encoding=UTF-8
+Name=SSH
+GenericName=Secure Shell
+Comment=Connect to a remote computer using Secure Shell
+Exec=hotwire-ssh
+Terminal=false
+Type=Application
+Icon=hotwire-openssh
+Categories=GNOME;GTK;Network;
+StartupNotify=true

Added: trunk/hotwire-ssh.sh
==============================================================================
--- (empty file)
+++ trunk/hotwire-ssh.sh	Tue May 20 15:11:46 2008
@@ -0,0 +1,6 @@
+if [ -n "$BASH_VERSION" -o -n "$KSH_VERSION" -o -n "$ZSH_VERSION" ]; then
+  [ -x //usr/bin/id ] || return
+  [ `//usr/bin/id -u` -le 100 ] && return
+  # for bash and zsh, only if no alias is already set
+  alias ssh >/dev/null 2>&1 || alias ssh='hotwire-ssh --bg'
+fi

Added: trunk/images/hotwire-openssh.png
==============================================================================
Binary file. No diff available.

Added: trunk/setup.py
==============================================================================
--- (empty file)
+++ trunk/setup.py	Tue May 20 15:11:46 2008
@@ -0,0 +1,109 @@
+# This file is part of the Hotwire Shell user interface.
+#   
+# Copyright (C) 2007,2008 Colin Walters <walters verbum org>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+import os,sys,subprocess
+from distutils.core import setup
+from distutils.command.install import install
+
+APPNAME = 'hotwire-ssh'
+MODDIR = 'hotssh'
+
+if __name__ == '__main__' and hasattr(sys.modules['__main__'], '__file__'):
+    basedir = os.path.dirname(os.path.abspath(__file__))
+    up_basedir = os.path.dirname(basedir)
+    if os.path.basename(basedir) == APPNAME:
+        print "Running uninstalled, extending path"
+        sys.path.insert(0, basedir)
+        os.environ['PYTHONPATH'] = os.pathsep.join(sys.path)
+def my_import(name):
+    mod = __import__(name)
+    components = name.split('.')
+    for comp in components[1:]:
+        mod = getattr(mod, comp)
+    return mod        
+ver = my_import(MODDIR + '.version')
+__version__ = getattr(ver, '__version__') 
+
+def svn_info(wd):
+    import subprocess,StringIO
+    tip = {}
+    for line in StringIO.StringIO(subprocess.Popen(['svn', 'info', wd], stdout=subprocess.PIPE).communicate()[0]):
+        line = line.strip()
+        if not line:
+            continue
+        (k,v) = line.split(':', 1)
+        tip[k.strip()] = v.strip()
+    return tip
+
+def svn_dist():
+    import subprocess,tempfile
+    import shutil
+
+    dt = os.path.join('dist', 'test')
+    try:
+        os.mkdir('dist')
+    except OSError, e:
+        pass
+    if os.path.exists(dt):
+        shutil.rmtree(dt)
+    subprocess.call(['svn', 'export', '.', dt])
+    oldwd = os.getcwd()
+    os.chdir(dt)
+    verfile = open(os.path.join(MODDIR, 'version.py'), 'a')
+    verfile.write('\n\n##AUTOGENERATED by setup.py##\nsvn_version_info = %s\n' % (repr(svn_info(oldwd)),))
+    verfile.close()
+    subprocess.call(['python', 'setup.py', 'sdist', '-k', '--format=zip'])
+
+def svn_dist_test():
+    import subprocess
+    svn_dist()
+    os.chdir('hotwire-' + __version__)
+    subprocess.call(['python', os.path.join('ui', 'test-hotwire')])
+
+if 'svn-dist' in sys.argv:
+    svn_dist()
+    sys.exit(0)
+elif 'svn-dist-test' in sys.argv:
+    # FIXME no test suite, below does not work
+    svn_dist_test()
+    sys.exit(0)
+
+kwargs = {'cmdclass': {}}
+kwargs['scripts'] = ['bin/hotwire-ssh']
+kwargs['data_files'] = [('share/applications', ['hotwire-ssh.desktop']), 
+                        ('share/icons/hicolor/24x24/apps', ['images/hotwire-openssh.png']),
+                        ('/etc/profile.d', ['hotwire-ssh.sh', 'hotwire-ssh.csh']),
+                       ]   
+    
+class HotInstall(install):
+    def run(self):
+        install.run(self)
+        if os.name == 'posix':                       
+            if self.root is None:
+                print "Running gtk-update-icon-cache"
+                subprocess.call(['gtk-update-icon-cache', os.path.join(self.install_data, 'icons')])
+kwargs['cmdclass']['install'] = HotInstall                    
+
+setup(name=APPNAME,
+      version=__version__,
+      description='Hotwire SSH',
+      author='Colin Walters',
+      author_email='walters verbum org',
+      url='http://hotwire-shell.org',   
+      packages=['hotssh', 'hotssh.hotlib', 'hotssh.hotlib_ui', 'hotssh.hotvte'],
+      **kwargs)



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]