Updated to conform to PEP-8, based on suggestions from SublimeLinter.

This commit is contained in:
Graeme Humphries 2012-03-22 16:41:39 -07:00
parent 634e514a6c
commit 24112ed67b

529
crussh.py
View File

@ -12,315 +12,318 @@ import json
import os.path import os.path
try: try:
import gtk import gtk
except: except:
print >>sys.stderr, "Missing Python GTK2 bindings." print >>sys.stderr, "Missing Python GTK2 bindings."
sys.exit(1) sys.exit(1)
try: try:
import vte import vte
except: except:
print >>sys.stderr, "Missing Python VTE bindings." print >>sys.stderr, "Missing Python VTE bindings."
sys.exit(1) sys.exit(1)
### Config Dialog ### ### Config Dialog ###
class CruSSHConf: class CruSSHConf:
### State Vars ### ### State Vars ###
Config = {} Config = {}
MainWin = gtk.Window() MainWin = gtk.Window()
### Signal Hooks ### ### Signal Hooks ###
def save_hook(self, discard, save_func): def save_hook(self, discard, save_func):
self.MainWin.destroy() self.MainWin.destroy()
if save_func is not None: if save_func is not None:
save_func(self.Config) save_func(self.Config)
def font_hook(self, fontbutton): def font_hook(self, fontbutton):
self.Config["font"] = fontbutton.get_font_name() self.Config["font"] = fontbutton.get_font_name()
def opacity_hook(self, range): def opacity_hook(self, range):
self.Config["opacity"] = range.get_value() self.Config["opacity"] = range.get_value()
def width_hook(self, spinbutton): def width_hook(self, spinbutton):
self.Config["min-width"] = spinbutton.get_value_as_int() self.Config["min-width"] = spinbutton.get_value_as_int()
def height_hook(self, spinbutton): def height_hook(self, spinbutton):
self.Config["min-height"] = spinbutton.get_value_as_int() self.Config["min-height"] = spinbutton.get_value_as_int()
### 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 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
self.MainWin.add(MainBox) self.MainWin.add(MainBox)
TermConfFrame = gtk.Frame(label="Terminal Options")
TermConfTable = gtk.Table(3, 2)
TermConfTable.props.border_width = 5
TermConfTable.props.row_spacing = 5
TermConfTable.props.column_spacing = 5
TermConfFrame.add(TermConfTable)
MainBox.pack_start(TermConfFrame)
TermConfTable.attach(gtk.Label("Font:"), 1, 2, 1, 2, gtk.EXPAND) TermConfFrame = gtk.Frame(label="Terminal Options")
FontConf = gtk.FontButton(fontname=self.Config["font"]) TermConfTable = gtk.Table(3, 2)
FontConf.connect("font-set", self.font_hook) TermConfTable.props.border_width = 5
TermConfTable.attach(FontConf, 2, 3, 1, 2, gtk.EXPAND) TermConfTable.props.row_spacing = 5
TermConfTable.props.column_spacing = 5
TermConfFrame.add(TermConfTable)
MainBox.pack_start(TermConfFrame)
SizeBox = gtk.HBox() TermConfTable.attach(gtk.Label("Font:"), 1, 2, 1, 2, gtk.EXPAND)
SizeBox.props.spacing = 5 FontConf = gtk.FontButton(fontname=self.Config["font"])
TermConfTable.attach(SizeBox, 1, 3, 2, 3) FontConf.connect("font-set", self.font_hook)
SizeBox.pack_start(gtk.Label("Min Width:"), fill=False, expand=False) TermConfTable.attach(FontConf, 2, 3, 1, 2, gtk.EXPAND)
WidthEntry = gtk.SpinButton(gtk.Adjustment(value=self.Config["min-width"], lower=1, upper=9999, step_incr=1))
WidthEntry.connect("value-changed", self.width_hook)
SizeBox.pack_start(WidthEntry, fill=False, expand=False)
SizeBox.pack_start(gtk.Label("Min Height:"), fill=False, expand=False)
HeightEntry = gtk.SpinButton(gtk.Adjustment(value=self.Config["min-height"], lower=1, upper=9999, step_incr=1))
HeightEntry.connect("value-changed", self.height_hook)
SizeBox.pack_start(HeightEntry, fill=False, expand=False)
TermConfTable.attach(gtk.Label("Opacity:"), 1, 2, 3, 4, gtk.EXPAND) SizeBox = gtk.HBox()
OpacityAdj = gtk.Adjustment(upper=65535, step_incr=1, value=self.Config["opacity"]) SizeBox.props.spacing = 5
OpacityScale = gtk.HScale(OpacityAdj) TermConfTable.attach(SizeBox, 1, 3, 2, 3)
OpacityScale.set_draw_value(False) SizeBox.pack_start(gtk.Label("Min Width:"), fill=False, expand=False)
# disconnect this until we get it working. WidthEntry = gtk.SpinButton(gtk.Adjustment(value=self.Config["min-width"], lower=1, upper=9999, step_incr=1))
# OpacityScale.connect("value-changed", self.opacity_hook) WidthEntry.connect("value-changed", self.width_hook)
TermConfTable.attach(OpacityScale, 2, 3, 3, 4) SizeBox.pack_start(WidthEntry, fill=False, expand=False)
SizeBox.pack_start(gtk.Label("Min Height:"), fill=False, expand=False)
HeightEntry = gtk.SpinButton(gtk.Adjustment(value=self.Config["min-height"], lower=1, upper=9999, step_incr=1))
HeightEntry.connect("value-changed", self.height_hook)
SizeBox.pack_start(HeightEntry, fill=False, expand=False)
ConfirmBox = gtk.HBox(spacing=5) TermConfTable.attach(gtk.Label("Opacity:"), 1, 2, 3, 4, gtk.EXPAND)
CancelButton = gtk.Button(stock=gtk.STOCK_CANCEL) OpacityAdj = gtk.Adjustment(upper=65535, step_incr=1, value=self.Config["opacity"])
ConfirmBox.pack_start(CancelButton, fill=False) OpacityScale = gtk.HScale(OpacityAdj)
SaveButton = gtk.Button(stock=gtk.STOCK_SAVE) OpacityScale.set_draw_value(False)
ConfirmBox.pack_start(SaveButton, fill=False) # disconnect this until we get it working.
MainBox.pack_start(ConfirmBox, fill=False, expand=False) # OpacityScale.connect("value-changed", self.opacity_hook)
TermConfTable.attach(OpacityScale, 2, 3, 3, 4)
# wire up behaviour
CancelButton.connect("clicked", lambda discard: self.MainWin.destroy())
SaveButton.connect("clicked", self.save_hook, save_func)
self.MainWin.show_all() ConfirmBox = gtk.HBox(spacing=5)
CancelButton = gtk.Button(stock=gtk.STOCK_CANCEL)
ConfirmBox.pack_start(CancelButton, fill=False)
SaveButton = gtk.Button(stock=gtk.STOCK_SAVE)
ConfirmBox.pack_start(SaveButton, fill=False)
MainBox.pack_start(ConfirmBox, fill=False, expand=False)
# we'll wire up a supplied save_func that takes the Config dict as an argument. # wire up behaviour
def __init__(self, config=None, save_func=None): CancelButton.connect("clicked", lambda discard: self.MainWin.destroy())
if config is not None: SaveButton.connect("clicked", self.save_hook, save_func)
self.Config = config
self.MainWin.show_all()
# we'll wire up a supplied save_func that takes the Config dict as an argument.
def __init__(self, config=None, save_func=None):
if config is not None:
self.Config = config
self.initGUI(save_func)
self.initGUI(save_func)
### CruSSH! ### ### CruSSH! ###
class CruSSH: class CruSSH:
### Config Vars ### ### Config Vars ###
# config defaults # config defaults
Config = { Config = {
"min-width": 80, "min-width": 80,
"min-height": 24, "min-height": 24,
"font": "Ubuntu Mono Bold 10", "font": "Ubuntu Mono Bold 10",
"opacity": 65535 "opacity": 65535
} }
### State Vars ### ### State Vars ###
Terminals = {} Terminals = {}
TermMinWidth = 1 TermMinWidth = 1
TermMinHeight = 1 TermMinHeight = 1
### GUI Objects ### ### GUI Objects ###
MainWin = gtk.Window() MainWin = gtk.Window()
ScrollWin = gtk.ScrolledWindow() ScrollWin = gtk.ScrolledWindow()
LayoutTable = gtk.Table() LayoutTable = gtk.Table()
EntryBox = gtk.Entry() EntryBox = gtk.Entry()
### Methods ### ### Methods ###
def reflowTable(self, cols=1, rows=1): def reflowTable(self, cols=1, rows=1):
# empty table and re-size # empty table and re-size
hosts = sorted(self.Terminals.keys(), reverse=True) hosts = sorted(self.Terminals.keys(), reverse=True)
for host in hosts: for host in hosts:
if self.Terminals[host].parent == self.LayoutTable: if self.Terminals[host].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
for row in range(rows): for row in range(rows):
for col in range(cols): for col in range(cols):
if len(hosts) > 0: if len(hosts) > 0:
host = hosts.pop() host = hosts.pop()
self.Terminals[host].set_size(self.Config["min-width"], self.Config["min-height"]) self.Terminals[host].set_size(self.Config["min-width"], self.Config["min-height"])
self.LayoutTable.attach(self.Terminals[host], col, col+1, row, row+1) self.LayoutTable.attach(self.Terminals[host], col, col + 1, row, row + 1)
def reflow(self, force=False): def reflow(self, force=False):
# reconfigure before updating rows and columns # reconfigure before updating rows and columns
self.configTerminals() self.configTerminals()
num_terms = len(self.Terminals.keys()) num_terms = len(self.Terminals.keys())
if num_terms < 1: if num_terms < 1:
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.allocation
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
elif cols > num_terms: elif cols > num_terms:
cols = num_terms cols = num_terms
rows = int(math.ceil(num_terms/float(cols))) rows = int(math.ceil(num_terms / float(cols)))
if rows < 1: if rows < 1:
rows = 1 rows = 1
if (self.LayoutTable.props.n_columns != cols) or (self.LayoutTable.props.n_rows != rows) or force: if (self.LayoutTable.props.n_columns != cols) or (self.LayoutTable.props.n_rows != rows) or force:
self.reflowTable(cols, rows) self.reflowTable(cols, rows)
self.MainWin.show_all() self.MainWin.show_all()
def configTerminals(self): def configTerminals(self):
for host in self.Terminals: for host in self.Terminals:
terminal = self.Terminals[host] terminal = self.Terminals[host]
# -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_from_string(self.Config["font"])
terminal.set_opacity(int(self.Config["opacity"])) terminal.set_opacity(int(self.Config["opacity"]))
self.TermMinWidth = (terminal.get_char_width() * self.Config["min-width"]) + terminal.get_padding()[0] self.TermMinWidth = (terminal.get_char_width() * self.Config["min-width"]) + terminal.get_padding()[0]
self.TermMinHeight = (terminal.get_char_height() * self.Config["min-height"]) + terminal.get_padding()[1] self.TermMinHeight = (terminal.get_char_height() * self.Config["min-height"]) + terminal.get_padding()[1]
def removeTerminal(self, terminal): def removeTerminal(self, terminal):
# brute force search since we don't actually know the hostname from the # brute force search since we don't actually know the hostname from the
# terminal object. this is an infrequent operation, so it should be fine. # terminal object. this is an infrequent operation, so it should be fine.
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])
del self.Terminals[host] 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.icon_theme_get_default()
icon_list = [] 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.ICON_LOOKUP_USE_BUILTIN) if icon is not None:
if icon is not None: gtk.window_set_default_icon(icon.load_icon())
gtk.window_set_default_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()) MainVBox = gtk.VBox()
MainVBox = gtk.VBox() 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, fill=True, expand=False)
FileItem = gtk.MenuItem(label="File") FileItem = gtk.MenuItem(label="File")
FileMenu = gtk.Menu() FileMenu = gtk.Menu()
FileItem.set_submenu(FileMenu) FileItem.set_submenu(FileMenu)
QuitItem = gtk.MenuItem(label="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)
EditItem = gtk.MenuItem(label="Edit") EditItem = gtk.MenuItem(label="Edit")
EditMenu = gtk.Menu() EditMenu = gtk.Menu()
EditItem.set_submenu(EditMenu) EditItem.set_submenu(EditMenu)
PrefsItem = gtk.MenuItem(label="Preferences") PrefsItem = gtk.MenuItem(label="Preferences")
def save_func(new_config):
self.Config = new_config
self.reflow(force=True)
# save to file last, so it doesn't hold up other GUI actions
conf_json = json.dumps(self.Config, sort_keys=True, indent=4)
try:
conf_file = open(os.path.expanduser("~/.crusshrc"), 'w')
conf_file.write(conf_json)
conf_file.close()
except:
pass
PrefsItem.connect("activate", lambda discard: CruSSHConf(self.Config, save_func))
EditMenu.append(PrefsItem)
MainMenuBar.append(EditItem)
self.ScrollWin.props.hscrollbar_policy = gtk.POLICY_NEVER def save_func(new_config):
self.ScrollWin.props.vscrollbar_policy = gtk.POLICY_ALWAYS self.Config = new_config
self.ScrollWin.props.shadow_type = gtk.SHADOW_ETCHED_IN self.reflow(force=True)
MainVBox.pack_start(self.ScrollWin) # save to file last, so it doesn't hold up other GUI actions
conf_json = json.dumps(self.Config, sort_keys=True, indent=4)
try:
conf_file = open(os.path.expanduser("~/.crusshrc"), 'w')
conf_file.write(conf_json)
conf_file.close()
except:
pass
PrefsItem.connect("activate", lambda discard: CruSSHConf(self.Config, save_func))
EditMenu.append(PrefsItem)
MainMenuBar.append(EditItem)
self.LayoutTable.set_homogeneous(True) self.ScrollWin.props.hscrollbar_policy = gtk.POLICY_NEVER
self.LayoutTable.set_row_spacings(1) self.ScrollWin.props.vscrollbar_policy = gtk.POLICY_ALWAYS
self.LayoutTable.set_col_spacings(1) self.ScrollWin.props.shadow_type = gtk.SHADOW_ETCHED_IN
self.ScrollWin.add_with_viewport(self.LayoutTable) MainVBox.pack_start(self.ScrollWin)
self.ScrollWin.set_size_request(self.TermMinWidth, self.TermMinHeight)
# don't display chars while typing. self.LayoutTable.set_homogeneous(True)
self.EntryBox.set_visibility(False) self.LayoutTable.set_row_spacings(1)
self.EntryBox.set_invisible_char(' ') self.LayoutTable.set_col_spacings(1)
# forward key events to all terminals self.ScrollWin.add_with_viewport(self.LayoutTable)
def feed_input(widget, event): self.ScrollWin.set_size_request(self.TermMinWidth, self.TermMinHeight)
if event.type in [gtk.gdk.KEY_PRESS, gtk.gdk.KEY_RELEASE]:
# erase buffer on every entry, so that passwords aren't revealed
self.EntryBox.props.buffer.delete_text(0, -1)
# propagate to every terminal
for host in self.Terminals:
t_event = event.copy()
self.Terminals[host].event(t_event)
# this stops regular handler from firing, switching focus.
return True
self.EntryBox.connect("key_press_event", feed_input)
self.EntryBox.connect("key_release_event", feed_input)
MainVBox.pack_start(self.EntryBox, False, False)
# reflow layout on size change. # don't display chars while typing.
self.MainWin.connect("size-allocate", lambda widget, allocation: self.reflow()) self.EntryBox.set_visibility(False)
self.EntryBox.set_invisible_char(' ')
# give EntryBox default focus on init # forward key events to all terminals
self.EntryBox.props.has_focus = True def feed_input(widget, event):
if event.type in [gtk.gdk.KEY_PRESS, gtk.gdk.KEY_RELEASE]:
# erase buffer on every entry, so that passwords aren't revealed
self.EntryBox.props.buffer.delete_text(0, -1)
# propagate to every terminal
for host in self.Terminals:
t_event = event.copy()
self.Terminals[host].event(t_event)
# this stops regular handler from firing, switching focus.
return True
self.EntryBox.connect("key_press_event", feed_input)
self.EntryBox.connect("key_release_event", feed_input)
MainVBox.pack_start(self.EntryBox, False, False)
def __init__(self, hosts, ssh_cmd="/usr/bin/ssh", ssh_args=None): # reflow layout on size change.
# load existing config file, if present self.MainWin.connect("size-allocate", lambda widget, allocation: self.reflow())
try:
# merge dicts to allow upgrade from old configs
new_config = json.load(open(os.path.expanduser('~/.crusshrc')))
self.Config.update(new_config)
except:
pass
# init all terminals # give EntryBox default focus on init
for host in hosts: self.EntryBox.props.has_focus = True
terminal = vte.Terminal()
# TODO: disable only this terminal widget on child exit
# v.connect("child-exited", lambda term: gtk.main_quit())
cmd_str = ssh_cmd
if ssh_args is not None:
cmd_str += " " + ssh_args
cmd_str += " " + host
cmd = cmd_str.split(' ')
terminal.fork_command(command=cmd[0], argv=cmd)
self.Terminals[host] = terminal
# hook terminals so they reflow layout on exit def __init__(self, hosts, ssh_cmd="/usr/bin/ssh", ssh_args=None):
self.Terminals[host].connect("child-exited", self.removeTerminal) # load existing config file, if present
# configure all terminals try:
self.configTerminals() # merge dicts to allow upgrade from old configs
# reflow after reconfig for font size changes new_config = json.load(open(os.path.expanduser('~/.crusshrc')))
self.initGUI() self.Config.update(new_config)
self.reflow(force=True) except:
pass
# init all terminals
for host in hosts:
terminal = vte.Terminal()
# TODO: disable only this terminal widget on child exit
# v.connect("child-exited", lambda term: gtk.main_quit())
cmd_str = ssh_cmd
if ssh_args is not None:
cmd_str += " " + ssh_args
cmd_str += " " + host
cmd = cmd_str.split(' ')
terminal.fork_command(command=cmd[0], argv=cmd)
self.Terminals[host] = terminal
# hook terminals so they reflow layout on exit
self.Terminals[host].connect("child-exited", self.removeTerminal)
# configure all terminals
self.configTerminals()
# reflow after reconfig for font size changes
self.initGUI()
self.reflow(force=True)
if __name__ == "__main__": if __name__ == "__main__":
import argparse import argparse
### Parse CLI Args ### ### Parse CLI Args ###
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: You can pass options to ssh if you add '--' before your list of hosts")
parser.add_argument("--ssh", dest='ssh', default="/usr/bin/ssh", parser.add_argument("--ssh", 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)")
(args, hosts) = parser.parse_known_args() (args, hosts) = parser.parse_known_args()
if len(hosts) == 0: if len(hosts) == 0:
parser.print_usage() parser.print_usage()
parser.exit(2) parser.exit(2)
try: try:
offset = hosts.index("--") offset = hosts.index("--")
except: except:
ssh_args = None ssh_args = None
else: else:
ssh_args = " ".join(hosts[0:offset]) ssh_args = " ".join(hosts[0:offset])
hosts = hosts[offset + 1:] hosts = hosts[offset + 1:]
### Start Execution ### ### Start Execution ###
crussh = CruSSH(hosts, args.ssh, ssh_args) crussh = CruSSH(hosts, args.ssh, ssh_args)
gtk.main() gtk.main()