merged from main

This commit is contained in:
Tessa Nordgren 2020-07-01 15:46:16 -07:00
commit 58a8402a91
3 changed files with 142 additions and 116 deletions

View File

@ -1,14 +1,18 @@
#!/usr/bin/env python #!/usr/bin/env python3
# This code taken from: # This code taken from:
# http://stackoverflow.com/questions/8290740/simple-versatile-and-re-usable-entry-dialog-sometimes-referred-to-as-input-dia # http://stackoverflow.com/questions/8290740/simple-versatile-and-re-usable-entry-dialog-sometimes-referred-to-as-input-dia
import gtk import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk as gtk
class EntryDialog(gtk.MessageDialog): class EntryDialog(gtk.MessageDialog):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
''' '''
Creates a new EntryDialog. Takes all the arguments of the usual Creates a new EntryDialog. Takes all the arguments of the usual
MessageDialog constructor plus one optional named argument MessageDialog constructor plus one optional named argument
"default_value" to specify the initial contents of the entry. "default_value" to specify the initial contents of the entry.
''' '''
if 'default_value' in kwargs: if 'default_value' in kwargs:
@ -17,20 +21,22 @@ class EntryDialog(gtk.MessageDialog):
else: else:
default_value = '' default_value = ''
super(EntryDialog, self).__init__(*args, **kwargs) super(EntryDialog, self).__init__(*args, **kwargs)
entry = gtk.Entry() entry = gtk.Entry()
entry.set_text(str(default_value)) entry.set_text(str(default_value))
entry.connect("activate", entry.connect("activate",
lambda ent, dlg, resp: dlg.response(resp), lambda ent, dlg, resp: dlg.response(resp),
self, gtk.RESPONSE_OK) self, gtk.ResponseType.OK)
self.vbox.pack_end(entry, True, True, 0) self.vbox.pack_end(entry, True, True, 0)
self.vbox.show_all() self.vbox.show_all()
self.entry = entry self.entry = entry
def set_value(self, text): def set_value(self, text):
self.entry.set_text(text) self.entry.set_text(text)
def run(self): def run(self):
result = super(EntryDialog, self).run() result = super(EntryDialog, self).run()
if result == gtk.RESPONSE_OK: if result == gtk.ResponseType.OK:
text = self.entry.get_text() text = self.entry.get_text()
else: else:
text = None text = None
return text return text

View File

@ -1,22 +1,12 @@
crussh: a modern cssh replacement # crussh: a modern cssh replacement
=================================
Backstory ## What are this?
---------
For anyone who needs to administrate clusters of many machines, For anyone who needs to administrate clusters of many machines,
[clusterssh](http://sourceforge.net/projects/clusterssh/) has long been a [clusterssh](http://sourceforge.net/projects/clusterssh/) has long been a
fallback for when the rest of your automation tools aren't working. fallback for when the rest of your automation tools aren't working.
However, cssh has a number of deficiencies in modern environments: crussh aims to be a simple replacement for cssh with the following improvements:
- Doesn't play nice with window placement of modern window managers.
- Doesn't play nice with modern toolkits' copy and paste behaviour.
- Gets a bit screwy when there's more terminals than can fit on-screen.
- Doesn't support nice antialiased fonts.
crussh aims to be a simple replacement for cssh that corrects these
problems. It does so with the following features:
- Uses a single window to hold multiple terminals. - Uses a single window to hold multiple terminals.
- Intelligently tiles terminals to fit available window size. - Intelligently tiles terminals to fit available window size.
@ -24,39 +14,56 @@ problems. It does so with the following features:
- Never resizes a terminal smaller than 80x24 characters. - Never resizes a terminal smaller than 80x24 characters.
- Uses GTK and the VTE widget to provide modern, anti-aliased terminals. - Uses GTK and the VTE widget to provide modern, anti-aliased terminals.
Install ## Install
-------
The install process is very simple on most distros: The install process is very simple on most distros:
- Install python2, python-gtk2, and python-vte. * Install python3 and python3-gi.
- Clone and symlink to your bin dir. * Clone and symlink to your bin dir:
```bash ```bash
git clone https://github.com/nergdron/crussh.git git clone https://github.com/nergdron/crussh.git
ln -s crussh/crussh.py ~/bin/crussh ln -s $(pwd)/crussh/crussh.py ~/bin/crussh
``` ```
- Run "crussh HOST [HOST ...]" Run ```crussh HOST [HOST ...]```
Usage Tips ## Examples
----------
Doing a clustered paste isn't completely obvious. The following methods will work: Basic usage is covered via the builtin help, which you can get by running
```crussh -h```. This section covers some common use cases.
To connect to a list of hosts in a file:
```bash
crussh $(cat hostlist.txt)
```
To use a custom login name, public key, or other SSH client options:
```bash
crussh -l someuser -i ~/.ssh/myotherkey -- host [host ...]
```
To do something other than ssh, such as edit a bunch of files in parallel:
```bash
crussh -e nano *.txt
```
## Usage Tips
Doing a clustered paste isn't completely obvious. The following methods will
work, after making sure you're clicked into the text entry box at the bottom of
the window:
- middle click or shift-insert to paste the X11 selection buffer. - middle click or shift-insert to paste the X11 selection buffer.
- control-shift-v to paste the GTK/GNOME clipboard. - control-shift-v to paste the GTK/GNOME clipboard.
Bugs & TODO ## Bugs & To Do
-----------
To see current issues, report problems, and see plans for features, To see current issues, report problems, and see plans for features,
see the [crussh GitHub issues page](https://github.com/nergdron/crussh/issues). see the [crussh GitHub issues page](https://github.com/nergdron/crussh/issues).
Copyright and License ## Copyright and License
---------------------
crussh is copyright 2012-2016 by Tessa Nordgren <tessa@sudo.ca>. crussh is copyright 2012-2019 by Tessa Nordgren <tessa@sudo.ca>.
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by

171
crussh.py
View File

@ -1,19 +1,21 @@
#!/usr/bin/python2 #!/usr/bin/env python3
# A cssh replacement written in Python / GTK. # A cssh replacement written in Python / GTK.
# (c)2012-2017 - Tessa Nordgren <tessa@sudo.ca>. # (c)2012-2019 - Tessa Nordgren <tessa@sudo.ca>.
# Released under the GPL, version 3: http://www.gnu.org/licenses/ # Released under the GPL, version 3: http://www.gnu.org/licenses/
# Requires: python-gtk2 python-vte import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk as gtk
from gi.repository import Gdk as gdk
from gi.repository import GLib as glib
from gi.repository.Pango import FontDescription
gi.require_version('Vte', '2.91')
from gi.repository import Vte as vte
import sys import sys
import math import math
import json import json
import os.path import os.path
import pygtk
pygtk.require('2.0')
import gtk
import vte
from EntryDialog import EntryDialog from EntryDialog import EntryDialog
from collections import OrderedDict from collections import OrderedDict
@ -45,7 +47,6 @@ class CruSSHConf:
### GUI Objects ### ### GUI Objects ###
def initGUI(self, save_func=None): def initGUI(self, save_func=None):
self.MainWin.set_modal(True) self.MainWin.set_modal(True)
self.MainWin.props.allow_grow = False
MainBox = gtk.VBox(spacing=5) MainBox = gtk.VBox(spacing=5)
MainBox.props.border_width = 5 MainBox.props.border_width = 5
@ -57,13 +58,13 @@ class CruSSHConf:
GlobalConfTable.props.row_spacing = 5 GlobalConfTable.props.row_spacing = 5
GlobalConfTable.props.column_spacing = 5 GlobalConfTable.props.column_spacing = 5
GlobalConfFrame.add(GlobalConfTable) GlobalConfFrame.add(GlobalConfTable)
MainBox.pack_start(GlobalConfFrame) MainBox.pack_start(GlobalConfFrame, False, False, 0)
GlobalConfTable.attach(gtk.Label("Start Maximized:"), 1, 2, 1, 2, gtk.EXPAND) GlobalConfTable.attach(gtk.Label("Start Maximized:"), 1, 2, 1, 2, gtk.AttachOptions.EXPAND)
MaximizedConf = gtk.CheckButton() MaximizedConf = gtk.CheckButton()
MaximizedConf.set_active(self.Config["start-maximized"]) MaximizedConf.set_active(self.Config["start-maximized"])
MaximizedConf.connect("toggled", self.maximized_hook) MaximizedConf.connect("toggled", self.maximized_hook)
GlobalConfTable.attach(MaximizedConf, 2, 3, 1, 2, gtk.EXPAND) GlobalConfTable.attach(MaximizedConf, 2, 3, 1, 2, gtk.AttachOptions.EXPAND)
TermConfFrame = gtk.Frame(label="Terminal Options") TermConfFrame = gtk.Frame(label="Terminal Options")
TermConfTable = gtk.Table(3, 2) TermConfTable = gtk.Table(3, 2)
@ -71,31 +72,34 @@ class CruSSHConf:
TermConfTable.props.row_spacing = 5 TermConfTable.props.row_spacing = 5
TermConfTable.props.column_spacing = 5 TermConfTable.props.column_spacing = 5
TermConfFrame.add(TermConfTable) TermConfFrame.add(TermConfTable)
MainBox.pack_start(TermConfFrame) MainBox.pack_start(TermConfFrame, False, False, 0)
TermConfTable.attach(gtk.Label("Font:"), 1, 2, 1, 2, gtk.EXPAND) TermConfTable.attach(gtk.Label("Font:"), 1, 2, 1, 2, gtk.AttachOptions.EXPAND)
FontConf = gtk.FontButton(fontname=self.Config["font"]) FontConf = gtk.FontButton(font=self.Config["font"])
FontConf.connect("font-set", self.font_hook) FontConf.connect("font-set", self.font_hook)
TermConfTable.attach(FontConf, 2, 3, 1, 2, gtk.EXPAND) TermConfTable.attach(FontConf, 2, 3, 1, 2, gtk.AttachOptions.EXPAND)
SizeBox = gtk.HBox() SizeBox = gtk.HBox()
SizeBox.props.spacing = 5 SizeBox.props.spacing = 5
TermConfTable.attach(SizeBox, 1, 3, 2, 3) TermConfTable.attach(SizeBox, 1, 3, 2, 3)
SizeBox.pack_start(gtk.Label("Min Width:"), fill=False, expand=False) SizeBox.pack_start(gtk.Label("Min Width:"), False, False, 0)
WidthEntry = gtk.SpinButton(gtk.Adjustment(value=self.Config["min-width"], lower=1, upper=9999, step_incr=1)) WidthEntry = gtk.SpinButton()
# gtk.Adjustment(value=self.Config["min-width"], lower=1, upper=9999, step_incr=1)
WidthEntry.set_range(1, 9999)
WidthEntry.connect("value-changed", self.width_hook) WidthEntry.connect("value-changed", self.width_hook)
SizeBox.pack_start(WidthEntry, fill=False, expand=False) SizeBox.pack_start(WidthEntry, False, False, 0)
SizeBox.pack_start(gtk.Label("Min Height:"), fill=False, expand=False) SizeBox.pack_start(gtk.Label("Min Height:"), False, False, 0)
HeightEntry = gtk.SpinButton(gtk.Adjustment(value=self.Config["min-height"], lower=1, upper=9999, step_incr=1)) HeightEntry = gtk.SpinButton()
HeightEntry.set_range(1, 9999)
HeightEntry.connect("value-changed", self.height_hook) HeightEntry.connect("value-changed", self.height_hook)
SizeBox.pack_start(HeightEntry, fill=False, expand=False) SizeBox.pack_start(HeightEntry, False, False, 0)
ConfirmBox = gtk.HBox(spacing=5) ConfirmBox = gtk.HBox(spacing=5)
CancelButton = gtk.Button(stock=gtk.STOCK_CANCEL) CancelButton = gtk.Button(stock=gtk.STOCK_CANCEL)
ConfirmBox.pack_start(CancelButton, fill=False) ConfirmBox.pack_start(CancelButton, False, False, 0)
SaveButton = gtk.Button(stock=gtk.STOCK_SAVE) SaveButton = gtk.Button(stock=gtk.STOCK_SAVE)
ConfirmBox.pack_start(SaveButton, fill=False) ConfirmBox.pack_start(SaveButton, False, False, 0)
MainBox.pack_start(ConfirmBox, fill=False, expand=False) MainBox.pack_start(ConfirmBox, False, False, 0)
# wire up behaviour # wire up behaviour
CancelButton.connect("clicked", lambda discard: self.MainWin.destroy()) CancelButton.connect("clicked", lambda discard: self.MainWin.destroy())
@ -121,7 +125,6 @@ class HostsMask:
def InitGUI(self): def InitGUI(self):
self.MainWin.set_modal(True) self.MainWin.set_modal(True)
self.MainWin.props.allow_grow = False
MainBox = gtk.VBox(spacing=5) MainBox = gtk.VBox(spacing=5)
MainBox.props.border_width = 5 MainBox.props.border_width = 5
@ -144,20 +147,20 @@ class HostsMask:
for host in hosts: for host in hosts:
HostTable = gtk.Table(1, 2) HostTable = gtk.Table(1, 2)
HostTable.props.column_spacing = 2 HostTable.props.column_spacing = 2
HostTable.attach(gtk.Label(host), 0, 1, 0, 1, gtk.EXPAND) HostTable.attach(gtk.Label(host), 0, 1, 0, 1, gtk.AttachOptions.EXPAND, gtk.AttachOptions.EXPAND, 0, 0)
HostCheckbox = gtk.CheckButton() HostCheckbox = gtk.CheckButton()
HostCheckbox.set_active(self.Terminals[host].copy_input) HostCheckbox.set_active(self.Terminals[host].copy_input)
HostCheckbox.connect("toggled", self.toggle_func, host) HostCheckbox.connect("toggled", self.toggle_func, host)
HostTable.attach(HostCheckbox, 1, 2, 0, 1, gtk.EXPAND) HostTable.attach(HostCheckbox, 1, 2, 0, 1,gtk.AttachOptions.EXPAND, gtk.AttachOptions.EXPAND, 0, 0)
row = i / cols row = i / cols
col = i % cols col = i % cols
HostsConfTable.attach(HostTable, col, col+1, row, row+1, gtk.EXPAND) HostsConfTable.attach(HostTable, col, col+1, row, row+1, gtk.AttachOptions.EXPAND, gtk.AttachOptions.EXPAND, 0, 0)
i += 1 i += 1
MainBox.pack_start(HostsConfFrame) MainBox.pack_start(HostsConfFrame, False, False, 0)
OkButton = gtk.Button(stock=gtk.STOCK_OK) OkButton = gtk.Button(stock=gtk.STOCK_OK)
MainBox.pack_start(OkButton, fill=False, expand=False) MainBox.pack_start(OkButton, False, False, 0)
# wire up behaviour # wire up behaviour
OkButton.connect("clicked", lambda discard: self.MainWin.destroy()) OkButton.connect("clicked", lambda discard: self.MainWin.destroy())
@ -177,8 +180,8 @@ class CruSSH:
Config = { Config = {
"min-width": 80, "min-width": 80,
"min-height": 24, "min-height": 24,
"font": "Ubuntu Mono Bold 10", "font": "Ubuntu Mono,monospace Bold 10",
"start-maximized": False "start-maximized": True
} }
### State Vars ### ### State Vars ###
@ -199,7 +202,7 @@ class CruSSH:
hosts = list(self.Terminals.keys()) hosts = list(self.Terminals.keys())
hosts.reverse() hosts.reverse()
for host in hosts: for host in hosts:
if self.Terminals[host].parent == self.LayoutTable: if self.Terminals[host].get_parent() == self.LayoutTable:
self.LayoutTable.remove(self.Terminals[host]) self.LayoutTable.remove(self.Terminals[host])
self.LayoutTable.resize(rows, cols) self.LayoutTable.resize(rows, cols)
# layout terminals # layout terminals
@ -220,7 +223,7 @@ class CruSSH:
gtk.main_quit() gtk.main_quit()
# main_quit desn't happen immediately # main_quit desn't happen immediately
return False return False
size = self.MainWin.allocation size = self.MainWin.get_size()
cols = int(math.floor((size.width + self.LayoutTable.props.column_spacing) / float(self.TermMinWidth))) cols = int(math.floor((size.width + self.LayoutTable.props.column_spacing) / float(self.TermMinWidth)))
if cols < 1 or num_terms == 1: if cols < 1 or num_terms == 1:
cols = 1 cols = 1
@ -239,28 +242,30 @@ class CruSSH:
def addHost(self, host): def addHost(self, host):
def handle_copy_paste(widget, event): def handle_copy_paste(widget, event):
self.EntryBox.props.buffer.delete_text(0, -1) self.EntryBox.props.buffer.delete_text(0, -1)
# check for paste key shortcut (ctl-shift-v) # check for paste key shortcut (ctl-shift-v)
if (event.type == gtk.gdk.KEY_PRESS) \ if (event.type == gdk.EventType.KEY_PRESS) \
and (event.state & gtk.gdk.CONTROL_MASK == gtk.gdk.CONTROL_MASK) \ and (event.state & gdk.ModifierType.CONTROL_MASK == gdk.ModifierType.CONTROL_MASK) \
and (event.state & gtk.gdk.SHIFT_MASK == gtk.gdk.SHIFT_MASK) \ and (event.state & gdk.ModifierType.SHIFT_MASK == gdk.ModifierType.SHIFT_MASK) \
and (event.keyval == gtk.gdk.keyval_from_name('V')): and (event.keyval == gdk.keyval_from_name('V')):
widget.paste_clipboard() widget.paste_clipboard()
return True return True
elif (event.type == gtk.gdk.KEY_PRESS) \ elif (event.type == gdk.EventType.KEY_PRESS) \
and (event.state & gtk.gdk.CONTROL_MASK == gtk.gdk.CONTROL_MASK) \ and (event.state & gdk.ModifierType.CONTROL_MASK == gdk.ModifierType.CONTROL_MASK) \
and (event.state & gtk.gdk.SHIFT_MASK == gtk.gdk.SHIFT_MASK) \ and (event.state & gdk.ModifierType.SHIFT_MASK == gdk.ModifierType.SHIFT_MASK) \
and (event.keyval == gtk.gdk.keyval_from_name('C')): and (event.keyval == gdk.keyval_from_name('C')):
widget.copy_clipboard() widget.copy_clipboard()
return True return True
terminal = vte.Terminal() terminal = vte.Terminal()
# TODO: disable only this terminal widget on child exit terminal.spawn_sync(
# v.connect("child-exited", lambda term: gtk.main_quit()) vte.PtyFlags.DEFAULT,
cmd_str = self.ssh_cmd None,
if self.ssh_args is not None: [self.ssh_cmd] + self.ssh_args + [host],
cmd_str += " " + self.ssh_args [],
cmd_str += " " + host glib.SpawnFlags.DO_NOT_REAP_CHILD,
cmd = cmd_str.split(' ') None,
terminal.fork_command(command=cmd[0], argv=cmd) None,
)
# track whether we mirror output to this terminal # track whether we mirror output to this terminal
terminal.copy_input = True terminal.copy_input = True
# attach copy/paste handler # attach copy/paste handler
@ -275,25 +280,28 @@ class CruSSH:
# -1 == infinite scrollback # -1 == infinite scrollback
terminal.set_scrollback_lines(-1) terminal.set_scrollback_lines(-1)
terminal.set_size(self.Config["min-width"], self.Config["min-height"]) terminal.set_size(self.Config["min-width"], self.Config["min-height"])
terminal.set_font_from_string(self.Config["font"]) terminal.set_font(FontDescription(self.Config["font"]))
self.TermMinWidth = (terminal.get_char_width() * self.Config["min-width"]) + terminal.get_padding()[0] self.TermMinWidth = terminal.get_char_width() * self.Config["min-width"]
self.TermMinHeight = (terminal.get_char_height() * self.Config["min-height"]) + terminal.get_padding()[1] self.TermMinHeight = terminal.get_char_height() * self.Config["min-height"]
def removeTerminal(self, terminal): def removeTerminal(self, terminal, status):
to_del = []
for host in self.Terminals.keys(): for host in self.Terminals.keys():
if terminal == self.Terminals[host]: if terminal == self.Terminals[host]:
self.LayoutTable.remove(self.Terminals[host]) self.LayoutTable.remove(self.Terminals[host])
print("Disconnected from " + host) print("Disconnected from " + host)
del self.Terminals[host] to_del.append(host)
for host in to_del:
del self.Terminals[host]
self.reflow(force=True) self.reflow(force=True)
def initGUI(self): def initGUI(self):
theme = gtk.icon_theme_get_default() theme = gtk.IconTheme.get_default()
if theme.has_icon("terminal"): if theme.has_icon("terminal"):
icon = theme.lookup_icon("terminal", 128, flags=gtk.ICON_LOOKUP_USE_BUILTIN) icon = theme.lookup_icon("terminal", 128, flags=gtk.IconLookupFlags.USE_BUILTIN)
if icon is not None: if icon is not None:
gtk.window_set_default_icon(icon.load_icon()) self.MainWin.set_icon(icon.load_icon())
self.MainWin.set_title("crussh: " + ' '.join(self.Terminals.keys())) self.MainWin.set_title("crussh: " + ' '.join(self.Terminals.keys()))
self.MainWin.set_role(role="crussh_main_win") self.MainWin.set_role(role="crussh_main_win")
self.MainWin.connect("delete-event", lambda window, event: gtk.main_quit()) self.MainWin.connect("delete-event", lambda window, event: gtk.main_quit())
@ -301,14 +309,15 @@ class CruSSH:
self.MainWin.add(MainVBox) self.MainWin.add(MainVBox)
MainMenuBar = gtk.MenuBar() MainMenuBar = gtk.MenuBar()
MainVBox.pack_start(MainMenuBar, fill=True, expand=False) MainVBox.pack_start(MainMenuBar, False, False, 0)
def add_host_handler(self, base): def add_host_handler(self, base):
diag = EntryDialog(buttons=gtk.BUTTONS_OK, type=gtk.MESSAGE_QUESTION, diag = EntryDialog(self.get_parent().get_parent(),
gtk.DialogFlags.MODAL | gtk.DialogFlags.DESTROY_WITH_PARENT,
gtk.MessageType.QUESTION, gtk.ButtonsType.OK_CANCEL,
message_format="Hostname to add:") message_format="Hostname to add:")
print "test"
host = diag.run() host = diag.run()
if len(host) > 0: if host is not None and len(host) > 0:
base.addHost(host) base.addHost(host)
diag.destroy() diag.destroy()
base.reflow(force=True) base.reflow(force=True)
@ -320,7 +329,7 @@ class CruSSH:
AddHostItem.connect("activate", add_host_handler, self) AddHostItem.connect("activate", add_host_handler, self)
FileMenu.append(AddHostItem) FileMenu.append(AddHostItem)
FileMenu.append(gtk.SeparatorMenuItem()) FileMenu.append(gtk.SeparatorMenuItem())
QuitItem = gtk.ImageMenuItem(gtk.STOCK_QUIT) QuitItem = gtk.MenuItem(label="Quit")
QuitItem.connect("activate", lambda discard: gtk.main_quit()) QuitItem.connect("activate", lambda discard: gtk.main_quit())
FileMenu.append(QuitItem) FileMenu.append(QuitItem)
MainMenuBar.append(FileItem) MainMenuBar.append(FileItem)
@ -350,15 +359,15 @@ class CruSSH:
EditMenu.append(PrefsItem) EditMenu.append(PrefsItem)
MainMenuBar.append(EditItem) MainMenuBar.append(EditItem)
self.ScrollWin.props.hscrollbar_policy = gtk.POLICY_NEVER self.ScrollWin.props.hscrollbar_policy = gtk.PolicyType.NEVER
self.ScrollWin.props.vscrollbar_policy = gtk.POLICY_ALWAYS self.ScrollWin.props.vscrollbar_policy = gtk.PolicyType.ALWAYS
self.ScrollWin.props.shadow_type = gtk.SHADOW_ETCHED_IN self.ScrollWin.props.shadow_type = gtk.ShadowType.ETCHED_IN
MainVBox.pack_start(self.ScrollWin) MainVBox.pack_start(self.ScrollWin, True, True, 0)
self.LayoutTable.set_homogeneous(True) self.LayoutTable.set_homogeneous(True)
self.LayoutTable.set_row_spacings(1) self.LayoutTable.set_row_spacings(1)
self.LayoutTable.set_col_spacings(1) self.LayoutTable.set_col_spacings(1)
self.ScrollWin.add_with_viewport(self.LayoutTable) self.ScrollWin.add(self.LayoutTable)
self.ScrollWin.set_size_request(self.TermMinWidth, self.TermMinHeight) self.ScrollWin.set_size_request(self.TermMinWidth, self.TermMinHeight)
# don't display chars while typing. # don't display chars while typing.
@ -376,10 +385,11 @@ class CruSSH:
def feed_input(widget, event): def feed_input(widget, event):
self.EntryBox.props.buffer.delete_text(0, -1) self.EntryBox.props.buffer.delete_text(0, -1)
# check for paste key shortcut (ctl-shift-v) # check for paste key shortcut (ctl-shift-v)
if (event.type == gtk.gdk.KEY_PRESS) \
and (event.state & gtk.gdk.CONTROL_MASK == gtk.gdk.CONTROL_MASK) \ if (event.type == gdk.EventType.KEY_PRESS) \
and (event.state & gtk.gdk.SHIFT_MASK == gtk.gdk.SHIFT_MASK) \ and (event.state & gdk.ModifierType.CONTROL_MASK == gdk.ModifierType.CONTROL_MASK) \
and (event.keyval == gtk.gdk.keyval_from_name('V')): and (event.state & gdk.ModifierType.SHIFT_MASK == gdk.ModifierType.SHIFT_MASK) \
and (event.get_keyval == gdk.keyval_from_name('V')):
feed_paste(widget) feed_paste(widget)
else: else:
# propagate to every terminal # propagate to every terminal
@ -399,7 +409,7 @@ class CruSSH:
self.EntryBox.connect("key_release_event", feed_input) self.EntryBox.connect("key_release_event", feed_input)
self.EntryBox.connect("paste_clipboard", feed_paste) self.EntryBox.connect("paste_clipboard", feed_paste)
self.EntryBox.connect("button_press_event", click_handler) self.EntryBox.connect("button_press_event", click_handler)
MainVBox.pack_start(self.EntryBox, False, False) MainVBox.pack_start(self.EntryBox, False, False, 0)
# reflow layout on size change. # reflow layout on size change.
self.MainWin.connect("size-allocate", lambda widget, allocation: self.reflow()) self.MainWin.connect("size-allocate", lambda widget, allocation: self.reflow())
@ -407,7 +417,7 @@ class CruSSH:
# give EntryBox default focus on init # give EntryBox default focus on init
self.EntryBox.props.has_focus = True self.EntryBox.props.has_focus = True
def __init__(self, hosts, ssh_cmd="/usr/bin/ssh", ssh_args=None): def __init__(self, hosts, ssh_cmd="/usr/bin/ssh", ssh_args=[]):
self.ssh_cmd = ssh_cmd self.ssh_cmd = ssh_cmd
self.ssh_args = ssh_args self.ssh_args = ssh_args
# load existing config file, if present # load existing config file, if present
@ -419,8 +429,11 @@ class CruSSH:
pass pass
# init all terminals # init all terminals
for host in hosts: if isinstance(hosts, list):
self.addHost(host) for host in hosts:
self.addHost(host)
else:
self.addHost(str(hosts))
# configure all terminals # configure all terminals
self.configTerminals() self.configTerminals()
# reflow after reconfig for font size changes # reflow after reconfig for font size changes
@ -437,7 +450,7 @@ if __name__ == "__main__":
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Connect to multiple hosts in parallel.", description="Connect to multiple hosts in parallel.",
usage="%(prog)s [OPTIONS] [--] HOST [HOST ...]", usage="%(prog)s [OPTIONS] [--] HOST [HOST ...]",
epilog="* NOTE: You can pass options to ssh if you add '--' before your list of hosts") epilog="* NOTE: options before '--' will be passed directly to the SSH client.")
parser.add_argument("--ssh", "-s", dest='ssh', default="/usr/bin/ssh", parser.add_argument("--ssh", "-s", dest='ssh', default="/usr/bin/ssh",
help="specify the SSH executable to use (default: %(default)s)") help="specify the SSH executable to use (default: %(default)s)")
parser.add_argument("--hosts-file", "-f", dest='hosts_file', default="", parser.add_argument("--hosts-file", "-f", dest='hosts_file', default="",
@ -457,7 +470,7 @@ if __name__ == "__main__":
try: try:
offset = hosts.index("--") offset = hosts.index("--")
except: except:
ssh_args = None ssh_args = []
else: else:
ssh_args = " ".join(hosts[0:offset]) ssh_args = " ".join(hosts[0:offset])
hosts = hosts[offset + 1:] hosts = hosts[offset + 1:]