fixed, and upgraded to python 3!
This commit is contained in:
parent
8e5b2c53ad
commit
7cdcc7c6c1
@ -1,14 +1,18 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# This code taken from:
|
||||
# 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):
|
||||
def __init__(self, *args, **kwargs):
|
||||
'''
|
||||
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.
|
||||
'''
|
||||
if 'default_value' in kwargs:
|
||||
@ -17,20 +21,22 @@ class EntryDialog(gtk.MessageDialog):
|
||||
else:
|
||||
default_value = ''
|
||||
super(EntryDialog, self).__init__(*args, **kwargs)
|
||||
entry = gtk.Entry()
|
||||
entry = gtk.Entry()
|
||||
entry.set_text(str(default_value))
|
||||
entry.connect("activate",
|
||||
lambda ent, dlg, resp: dlg.response(resp),
|
||||
self, gtk.RESPONSE_OK)
|
||||
entry.connect("activate",
|
||||
lambda ent, dlg, resp: dlg.response(resp),
|
||||
self, gtk.ResponseType.OK)
|
||||
self.vbox.pack_end(entry, True, True, 0)
|
||||
self.vbox.show_all()
|
||||
self.entry = entry
|
||||
|
||||
def set_value(self, text):
|
||||
self.entry.set_text(text)
|
||||
|
||||
def run(self):
|
||||
result = super(EntryDialog, self).run()
|
||||
if result == gtk.RESPONSE_OK:
|
||||
if result == gtk.ResponseType.OK:
|
||||
text = self.entry.get_text()
|
||||
else:
|
||||
text = None
|
||||
return text
|
||||
return text
|
||||
|
@ -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:
|
||||
|
||||
* Install python2, python-gtk2, and python-vte.
|
||||
* Install python3 and python3-gi.
|
||||
* Clone and symlink to your bin dir:
|
||||
```bash
|
||||
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 ...]```
|
||||
|
165
crussh.py
165
crussh.py
@ -1,17 +1,21 @@
|
||||
#!/usr/bin/python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# A cssh replacement written in Python / GTK.
|
||||
# (c)2012-2019 - Tessa Nordgren <tessa@sudo.ca>.
|
||||
# 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 math
|
||||
import json
|
||||
import os.path
|
||||
import pygtk
|
||||
pygtk.require('2.0')
|
||||
import gtk
|
||||
import vte
|
||||
from EntryDialog import EntryDialog
|
||||
|
||||
|
||||
@ -42,7 +46,6 @@ class CruSSHConf:
|
||||
### GUI Objects ###
|
||||
def initGUI(self, save_func=None):
|
||||
self.MainWin.set_modal(True)
|
||||
self.MainWin.props.allow_grow = False
|
||||
|
||||
MainBox = gtk.VBox(spacing=5)
|
||||
MainBox.props.border_width = 5
|
||||
@ -54,13 +57,13 @@ class CruSSHConf:
|
||||
GlobalConfTable.props.row_spacing = 5
|
||||
GlobalConfTable.props.column_spacing = 5
|
||||
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.set_active(self.Config["start-maximized"])
|
||||
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")
|
||||
TermConfTable = gtk.Table(3, 2)
|
||||
@ -68,31 +71,34 @@ class CruSSHConf:
|
||||
TermConfTable.props.row_spacing = 5
|
||||
TermConfTable.props.column_spacing = 5
|
||||
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)
|
||||
FontConf = gtk.FontButton(fontname=self.Config["font"])
|
||||
TermConfTable.attach(gtk.Label("Font:"), 1, 2, 1, 2, gtk.AttachOptions.EXPAND)
|
||||
FontConf = gtk.FontButton(font=self.Config["font"])
|
||||
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.props.spacing = 5
|
||||
TermConfTable.attach(SizeBox, 1, 3, 2, 3)
|
||||
SizeBox.pack_start(gtk.Label("Min Width:"), fill=False, expand=False)
|
||||
WidthEntry = gtk.SpinButton(gtk.Adjustment(value=self.Config["min-width"], lower=1, upper=9999, step_incr=1))
|
||||
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.set_range(1, 9999)
|
||||
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))
|
||||
SizeBox.pack_start(WidthEntry, False, False, 0)
|
||||
SizeBox.pack_start(gtk.Label("Min Height:"), False, False, 0)
|
||||
HeightEntry = gtk.SpinButton()
|
||||
HeightEntry.set_range(1, 9999)
|
||||
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)
|
||||
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)
|
||||
ConfirmBox.pack_start(SaveButton, fill=False)
|
||||
MainBox.pack_start(ConfirmBox, fill=False, expand=False)
|
||||
ConfirmBox.pack_start(SaveButton, False, False, 0)
|
||||
MainBox.pack_start(ConfirmBox, False, False, 0)
|
||||
|
||||
# wire up behaviour
|
||||
CancelButton.connect("clicked", lambda discard: self.MainWin.destroy())
|
||||
@ -118,7 +124,6 @@ class HostsMask:
|
||||
|
||||
def InitGUI(self):
|
||||
self.MainWin.set_modal(True)
|
||||
self.MainWin.props.allow_grow = False
|
||||
|
||||
MainBox = gtk.VBox(spacing=5)
|
||||
MainBox.props.border_width = 5
|
||||
@ -140,20 +145,20 @@ class HostsMask:
|
||||
for host in hosts:
|
||||
HostTable = gtk.Table(1, 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.set_active(self.Terminals[host].copy_input)
|
||||
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
|
||||
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
|
||||
|
||||
MainBox.pack_start(HostsConfFrame)
|
||||
MainBox.pack_start(HostsConfFrame, False, False, 0)
|
||||
|
||||
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
|
||||
OkButton.connect("clicked", lambda discard: self.MainWin.destroy())
|
||||
@ -173,8 +178,8 @@ class CruSSH:
|
||||
Config = {
|
||||
"min-width": 80,
|
||||
"min-height": 24,
|
||||
"font": "Ubuntu Mono Bold 10",
|
||||
"start-maximized": False
|
||||
"font": "Ubuntu Mono,monospace Bold 10",
|
||||
"start-maximized": True
|
||||
}
|
||||
|
||||
### State Vars ###
|
||||
@ -194,7 +199,7 @@ class CruSSH:
|
||||
# empty table and re-size
|
||||
hosts = sorted(self.Terminals.keys(), reverse=True)
|
||||
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.resize(rows, cols)
|
||||
# layout terminals
|
||||
@ -215,7 +220,7 @@ class CruSSH:
|
||||
gtk.main_quit()
|
||||
# main_quit desn't happen immediately
|
||||
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)))
|
||||
if cols < 1 or num_terms == 1:
|
||||
cols = 1
|
||||
@ -234,28 +239,30 @@ class CruSSH:
|
||||
def addHost(self, host):
|
||||
def handle_copy_paste(widget, event):
|
||||
self.EntryBox.props.buffer.delete_text(0, -1)
|
||||
|
||||
# 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) \
|
||||
and (event.state & gtk.gdk.SHIFT_MASK == gtk.gdk.SHIFT_MASK) \
|
||||
and (event.keyval == gtk.gdk.keyval_from_name('V')):
|
||||
if (event.type == gdk.EventType.KEY_PRESS) \
|
||||
and (event.state & gdk.ModifierType.CONTROL_MASK == gdk.ModifierType.CONTROL_MASK) \
|
||||
and (event.state & gdk.ModifierType.SHIFT_MASK == gdk.ModifierType.SHIFT_MASK) \
|
||||
and (event.keyval == gdk.keyval_from_name('V')):
|
||||
widget.paste_clipboard()
|
||||
return True
|
||||
elif (event.type == gtk.gdk.KEY_PRESS) \
|
||||
and (event.state & gtk.gdk.CONTROL_MASK == gtk.gdk.CONTROL_MASK) \
|
||||
and (event.state & gtk.gdk.SHIFT_MASK == gtk.gdk.SHIFT_MASK) \
|
||||
and (event.keyval == gtk.gdk.keyval_from_name('C')):
|
||||
elif (event.type == gdk.EventType.KEY_PRESS) \
|
||||
and (event.state & gdk.ModifierType.CONTROL_MASK == gdk.ModifierType.CONTROL_MASK) \
|
||||
and (event.state & gdk.ModifierType.SHIFT_MASK == gdk.ModifierType.SHIFT_MASK) \
|
||||
and (event.keyval == gdk.keyval_from_name('C')):
|
||||
widget.copy_clipboard()
|
||||
return True
|
||||
terminal = vte.Terminal()
|
||||
# TODO: disable only this terminal widget on child exit
|
||||
# v.connect("child-exited", lambda term: gtk.main_quit())
|
||||
cmd_str = self.ssh_cmd
|
||||
if self.ssh_args is not None:
|
||||
cmd_str += " " + self.ssh_args
|
||||
cmd_str += " " + host
|
||||
cmd = cmd_str.split(' ')
|
||||
terminal.fork_command(command=cmd[0], argv=cmd)
|
||||
terminal.spawn_sync(
|
||||
vte.PtyFlags.DEFAULT,
|
||||
None,
|
||||
[self.ssh_cmd] + self.ssh_args + [host],
|
||||
[],
|
||||
glib.SpawnFlags.DO_NOT_REAP_CHILD,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
# track whether we mirror output to this terminal
|
||||
terminal.copy_input = True
|
||||
# attach copy/paste handler
|
||||
@ -270,25 +277,28 @@ class CruSSH:
|
||||
# -1 == infinite scrollback
|
||||
terminal.set_scrollback_lines(-1)
|
||||
terminal.set_size(self.Config["min-width"], self.Config["min-height"])
|
||||
terminal.set_font_from_string(self.Config["font"])
|
||||
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]
|
||||
terminal.set_font(FontDescription(self.Config["font"]))
|
||||
self.TermMinWidth = terminal.get_char_width() * self.Config["min-width"]
|
||||
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():
|
||||
if terminal == self.Terminals[host]:
|
||||
self.LayoutTable.remove(self.Terminals[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)
|
||||
|
||||
def initGUI(self):
|
||||
theme = gtk.icon_theme_get_default()
|
||||
theme = gtk.IconTheme.get_default()
|
||||
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:
|
||||
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_role(role="crussh_main_win")
|
||||
self.MainWin.connect("delete-event", lambda window, event: gtk.main_quit())
|
||||
@ -296,14 +306,15 @@ class CruSSH:
|
||||
self.MainWin.add(MainVBox)
|
||||
|
||||
MainMenuBar = gtk.MenuBar()
|
||||
MainVBox.pack_start(MainMenuBar, fill=True, expand=False)
|
||||
MainVBox.pack_start(MainMenuBar, False, False, 0)
|
||||
|
||||
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:")
|
||||
print "test"
|
||||
host = diag.run()
|
||||
if len(host) > 0:
|
||||
if host is not None and len(host) > 0:
|
||||
base.addHost(host)
|
||||
diag.destroy()
|
||||
base.reflow(force=True)
|
||||
@ -315,7 +326,7 @@ class CruSSH:
|
||||
AddHostItem.connect("activate", add_host_handler, self)
|
||||
FileMenu.append(AddHostItem)
|
||||
FileMenu.append(gtk.SeparatorMenuItem())
|
||||
QuitItem = gtk.ImageMenuItem(gtk.STOCK_QUIT)
|
||||
QuitItem = gtk.MenuItem(label="Quit")
|
||||
QuitItem.connect("activate", lambda discard: gtk.main_quit())
|
||||
FileMenu.append(QuitItem)
|
||||
MainMenuBar.append(FileItem)
|
||||
@ -345,15 +356,15 @@ class CruSSH:
|
||||
EditMenu.append(PrefsItem)
|
||||
MainMenuBar.append(EditItem)
|
||||
|
||||
self.ScrollWin.props.hscrollbar_policy = gtk.POLICY_NEVER
|
||||
self.ScrollWin.props.vscrollbar_policy = gtk.POLICY_ALWAYS
|
||||
self.ScrollWin.props.shadow_type = gtk.SHADOW_ETCHED_IN
|
||||
MainVBox.pack_start(self.ScrollWin)
|
||||
self.ScrollWin.props.hscrollbar_policy = gtk.PolicyType.NEVER
|
||||
self.ScrollWin.props.vscrollbar_policy = gtk.PolicyType.ALWAYS
|
||||
self.ScrollWin.props.shadow_type = gtk.ShadowType.ETCHED_IN
|
||||
MainVBox.pack_start(self.ScrollWin, True, True, 0)
|
||||
|
||||
self.LayoutTable.set_homogeneous(True)
|
||||
self.LayoutTable.set_row_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)
|
||||
|
||||
# don't display chars while typing.
|
||||
@ -371,10 +382,11 @@ class CruSSH:
|
||||
def feed_input(widget, event):
|
||||
self.EntryBox.props.buffer.delete_text(0, -1)
|
||||
# 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) \
|
||||
and (event.state & gtk.gdk.SHIFT_MASK == gtk.gdk.SHIFT_MASK) \
|
||||
and (event.keyval == gtk.gdk.keyval_from_name('V')):
|
||||
|
||||
if (event.type == gdk.EventType.KEY_PRESS) \
|
||||
and (event.state & gdk.ModifierType.CONTROL_MASK == gdk.ModifierType.CONTROL_MASK) \
|
||||
and (event.state & gdk.ModifierType.SHIFT_MASK == gdk.ModifierType.SHIFT_MASK) \
|
||||
and (event.get_keyval == gdk.keyval_from_name('V')):
|
||||
feed_paste(widget)
|
||||
else:
|
||||
# propagate to every terminal
|
||||
@ -394,7 +406,7 @@ class CruSSH:
|
||||
self.EntryBox.connect("key_release_event", feed_input)
|
||||
self.EntryBox.connect("paste_clipboard", feed_paste)
|
||||
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.
|
||||
self.MainWin.connect("size-allocate", lambda widget, allocation: self.reflow())
|
||||
@ -402,7 +414,7 @@ class CruSSH:
|
||||
# give EntryBox default focus on init
|
||||
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_args = ssh_args
|
||||
# load existing config file, if present
|
||||
@ -414,8 +426,11 @@ class CruSSH:
|
||||
pass
|
||||
|
||||
# init all terminals
|
||||
for host in hosts:
|
||||
self.addHost(host)
|
||||
if isinstance(hosts, list):
|
||||
for host in hosts:
|
||||
self.addHost(host)
|
||||
else:
|
||||
self.addHost(str(hosts))
|
||||
# configure all terminals
|
||||
self.configTerminals()
|
||||
# reflow after reconfig for font size changes
|
||||
@ -452,7 +467,7 @@ if __name__ == "__main__":
|
||||
try:
|
||||
offset = hosts.index("--")
|
||||
except:
|
||||
ssh_args = None
|
||||
ssh_args = []
|
||||
else:
|
||||
ssh_args = " ".join(hosts[0:offset])
|
||||
hosts = hosts[offset + 1:]
|
||||
|
Loading…
Reference in New Issue
Block a user