fixed, and upgraded to python 3!

This commit is contained in:
Tessa Nordgren 2019-06-02 05:51:06 -07:00
parent 8e5b2c53ad
commit 7cdcc7c6c1
3 changed files with 107 additions and 86 deletions

View File

@ -1,9 +1,13 @@
#!/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):
''' '''
@ -21,15 +25,17 @@ class EntryDialog(gtk.MessageDialog):
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

View File

@ -18,11 +18,11 @@ crussh aims to be a simple replacement for cssh with the following improvements:
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 ...]```

165
crussh.py
View File

@ -1,17 +1,21 @@
#!/usr/bin/python #!/usr/bin/env python3
# A cssh replacement written in Python / GTK. # A cssh replacement written in Python / GTK.
# (c)2012-2019 - 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/
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
@ -42,7 +46,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
@ -54,13 +57,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)
@ -68,31 +71,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())
@ -118,7 +124,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
@ -140,20 +145,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())
@ -173,8 +178,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 ###
@ -194,7 +199,7 @@ class CruSSH:
# 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].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
@ -215,7 +220,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
@ -234,28 +239,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
@ -270,25 +277,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())
@ -296,14 +306,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)
@ -315,7 +326,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)
@ -345,15 +356,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.
@ -371,10 +382,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
@ -394,7 +406,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())
@ -402,7 +414,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
@ -414,8 +426,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
@ -452,7 +467,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:]