hotwire-ssh r2 - in trunk: . bin hotssh hotssh/hotlib hotssh/hotlib_ui hotssh/hotvte images
- From: walters svn gnome org
- To: svn-commits-list gnome org
- Subject: hotwire-ssh r2 - in trunk: . bin hotssh hotssh/hotlib hotssh/hotlib_ui hotssh/hotvte images
- Date: Tue, 20 May 2008 15:11:47 +0000 (UTC)
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]