diff --git a/EntryDialog.py b/EntryDialog.py index d9ec5df..84acd4d 100644 --- a/EntryDialog.py +++ b/EntryDialog.py @@ -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 \ No newline at end of file + return text diff --git a/README.md b/README.md index a68ba35..33a2cb6 100644 --- a/README.md +++ b/README.md @@ -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 ...]``` diff --git a/crussh.py b/crussh.py index eb79ae2..c23a873 100755 --- a/crussh.py +++ b/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 . # 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:]