From 57742c02c8bc9dd4db6f14396f2f641bba56dfb0 Mon Sep 17 00:00:00 2001 From: Mario Manno Date: Sun, 31 Jan 2021 02:20:49 +0100 Subject: [PATCH 01/29] Add tmux support This is squashes the original commits from launchpad in 2016, since I was unable to merge the complete history. The main work has been done by Andrea and Dan: https://bugs.launchpad.net/terminator/+bug/1301605 For reference, this branch has the history from lp: https://github.com/manno/terminator/tree/tmux Co-authored-by: Andrea Fagiani Co-authored-by: Dan Kilman --- INSTALL.md | 22 +- TMUX.md | 13 ++ setup.py | 1 + terminator | 10 +- terminatorlib/notebook.py | 4 +- terminatorlib/optionparse.py | 4 + terminatorlib/paned.py | 4 +- terminatorlib/terminal.py | 89 ++++++-- terminatorlib/terminator.py | 46 ++++- terminatorlib/tmux/__init__.py | 0 terminatorlib/tmux/control.py | 306 +++++++++++++++++++++++++++ terminatorlib/tmux/layout.py | 260 +++++++++++++++++++++++ terminatorlib/tmux/notifications.py | 310 ++++++++++++++++++++++++++++ terminatorlib/util.py | 39 +++- terminatorlib/window.py | 8 +- tests/test_tmux.py | 29 +++ 16 files changed, 1104 insertions(+), 41 deletions(-) create mode 100644 TMUX.md create mode 100644 terminatorlib/tmux/__init__.py create mode 100644 terminatorlib/tmux/control.py create mode 100644 terminatorlib/tmux/layout.py create mode 100644 terminatorlib/tmux/notifications.py create mode 100644 tests/test_tmux.py diff --git a/INSTALL.md b/INSTALL.md index b9ee5451e..45acaa712 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -8,7 +8,7 @@ Packages are known to be available under the name "terminator" under a lot of distributions, see below for a list. I also maintain a PPA for Ubuntu 20.04 and up that has the latest release -If you're running ubuntu 20.04 or later, you can run +If you're running ubuntu 20.04 or later, you can run ``` sudo add-apt-repository ppa:mattrose/terminator @@ -24,14 +24,20 @@ dependencies yourself: **Python 3.5+ recommended:** `python3` or `python37` (in FreeBSD) **Python GTK and VTE bindings:** - - Fedora/CentOS: python3-gobject python3-configobj python3-psutil vte291 + + Fedora/CentOS: python3-gobject python3-configobj python3-psutil vte291 keybinder3 intltool gettext - Debian/Ubuntu: python3-gi python3-gi-cairo python3-psutil python3-configobj - gir1.2-keybinder-3.0 gir1.2-vte-2.91 gettext intltool dbus-x11 - FreeBSD: py37-psutil py37-configobj keybinder-gtk3 py37-gobject3 gettext + Debian/Ubuntu: python3-gi python3-gi-cairo python3-psutil python3-configobj + gir1.2-keybinder-3.0 gir1.2-vte-2.91 gettext intltool dbus-x11 + FreeBSD: py37-psutil py37-configobj keybinder-gtk3 py37-gobject3 gettext intltool libnotify vte3 +**Python PyParsing library (only required for Tmux mode):** + + Debian/Ubuntu: python-pyparsing + + FreeBSD: devel/py-pyparsing + If you don't care about native language support or icons, Terminator should run just fine directly from this directory, just: @@ -43,7 +49,7 @@ And go from there. Manpages are available in the 'doc' directory. > make sure to update either the shebangs, call the scripts with `python3` or > use a wrapper script. > -> Setuptools install will update the scripts with the correct shebang. +> Setuptools install will update the scripts with the correct shebang. To install properly, run: @@ -74,7 +80,7 @@ Where ${PREFIX} is the base install directory; e.g. /usr/local. If you maintain terminator for an OS other than these, please get in touch or issue a PR to this file. -Distribution | Contact | Package Info | Source Code | Bug Tracker | +Distribution | Contact | Package Info | Source Code | Bug Tracker | -------------|---------|-----|-------------|-------------| ArchLinux | [@grazzolini] | [archlinux.org] | [git.archlinux.org] | [bugs.archlinux.org] CentOS EPEL | [@mattrose], [@dmaphy] | | [src.fedoraproject.org/branches] diff --git a/TMUX.md b/TMUX.md new file mode 100644 index 000000000..1af0f1f1a --- /dev/null +++ b/TMUX.md @@ -0,0 +1,13 @@ +# Tmux control mode + +Terminator support the [tmux control mode](http://man7.org/linux/man-pages/man1/tmux.1.html#CONTROL_MODE). + +Remote SSH example, starts tmux on remote host and displays tabs and splits in terminator: +``` +terminator -M --remote example.org +``` + +Local session: +``` +terminator -M +``` diff --git a/setup.py b/setup.py index c5347f463..463fa6007 100755 --- a/setup.py +++ b/setup.py @@ -230,6 +230,7 @@ def _find_css_files (self): packages=[ 'terminatorlib', 'terminatorlib.plugins', + 'terminatorlib.tmux', ], setup_requires=[ 'pytest-runner', diff --git a/terminator b/terminator index fe38748d8..1d360ebf7 100755 --- a/terminator +++ b/terminator @@ -23,6 +23,7 @@ import sys import os import psutil import pwd +import time try: ORIGCWD = os.getcwd() except OSError: @@ -88,8 +89,10 @@ if __name__ == '__main__': # continue. Failure to import dbus, or the global config option "dbus" # being False will cause us to continue without the dbus server and open a # window. + # Disable DBUS if using tmux, so we can have multiple sessions (e.g. local + # and remote, multiple remotes, etc.) try: - if OPTIONS.nodbus: + if OPTIONS.nodbus or OPTIONS.tmux: dbg('dbus disabled by command line') raise ImportError from terminatorlib import ipc @@ -125,12 +128,17 @@ if __name__ == '__main__': TERMINATOR.ibus_running = ibus_running try: + if OPTIONS.tmux: + TERMINATOR.start_tmux(remote=OPTIONS.remote) + while TERMINATOR.initial_layout is None: + time.sleep(0.1) dbg('Creating a terminal with layout: %s' % OPTIONS.layout) TERMINATOR.create_layout(OPTIONS.layout) except (KeyError,ValueError) as ex: err('layout creation failed, creating a window ("%s")' % ex) TERMINATOR.new_window() TERMINATOR.layout_done() + TERMINATOR.initial_layout = None if OPTIONS.debug and OPTIONS.debug >= 2: import terminatorlib.debugserver as debugserver diff --git a/terminatorlib/notebook.py b/terminatorlib/notebook.py index 52e70b9e9..5438c6353 100644 --- a/terminatorlib/notebook.py +++ b/terminatorlib/notebook.py @@ -164,7 +164,9 @@ def split_axis(self, widget, vertical=True, cwd=None, sibling=None, widgetfirst= sibling.set_cwd(cwd) if self.config['always_split_with_profile']: sibling.force_set_profile(None, widget.get_profile()) - sibling.spawn_child() + sibling.spawn_child( + orientation='vertical' if vertical else 'horizontal', + active_pane_id=getattr(widget, 'pane_id', None)) if widget.group and self.config['split_to_group']: sibling.set_group(None, widget.group) elif self.config['always_split_with_profile']: diff --git a/terminatorlib/optionparse.py b/terminatorlib/optionparse.py index 822f718ac..3e67d132f 100644 --- a/terminatorlib/optionparse.py +++ b/terminatorlib/optionparse.py @@ -57,6 +57,8 @@ def parse_options(): dest='borderless', help=_('Disable window borders')) parser.add_option('-H', '--hidden', action='store_true', dest='hidden', help=_('Hide the window at startup')) + parser.add_option('-M', '--tmux', action='store_true', + dest='tmux', help=_('Enable tmux integration')) parser.add_option('-T', '--title', dest='forcedtitle', help=_('Specify a title for the window')) parser.add_option('--geometry', dest='geometry', type='string', @@ -72,6 +74,8 @@ def parse_options(): callback=execute_cb, help=_('Use the rest of the command line as a command to ' 'execute inside the terminal, and its arguments')) + parser.add_option('--remote', dest='remote', + help=_('Specify a remote server for tmux to connect to')) parser.add_option('-g', '--config', dest='config', help=_('Specify a config file')) parser.add_option('-j', '--config-json', dest='configjson', diff --git a/terminatorlib/paned.py b/terminatorlib/paned.py index f13b2cbd1..1850d4c2b 100644 --- a/terminatorlib/paned.py +++ b/terminatorlib/paned.py @@ -52,7 +52,9 @@ def split_axis(self, widget, vertical=True, cwd=None, sibling=None, sibling.set_cwd(cwd) if self.config['always_split_with_profile']: sibling.force_set_profile(None, widget.get_profile()) - sibling.spawn_child() + sibling.spawn_child( + orientation='vertical' if vertical else 'horizontal', + active_pane_id=getattr(widget, 'pane_id', None)) if widget.group and self.config['split_to_group']: sibling.set_group(None, widget.group) elif self.config['always_split_with_profile']: diff --git a/terminatorlib/terminal.py b/terminatorlib/terminal.py index c76cd9a86..9273e5db3 100644 --- a/terminatorlib/terminal.py +++ b/terminatorlib/terminal.py @@ -21,6 +21,7 @@ from .config import Config from .cwd import get_pid_cwd from .factory import Factory +from pipes import quote from .terminator import Terminator from .titlebar import Titlebar from .terminal_popup_menu import TerminalPopupMenu @@ -140,6 +141,9 @@ class Terminal(Gtk.VBox): cnxids = None targets_for_new_group = None + control = None + pane_id = None + def __init__(self): """Class initialiser""" GObject.GObject.__init__(self) @@ -226,6 +230,8 @@ def __init__(self): self.reconfigure() self.vte.set_size(80, 24) + self.control = self.terminator.tmux_control + def get_vte(self): """This simply returns the vte widget we are using""" return(self.vte) @@ -280,7 +286,7 @@ def close(self): dbg('close: called') self.cnxids.remove_widget(self.vte) self.emit('close-term') - if self.pid is not None: + if not self.terminator.tmux_control: try: dbg('close: killing %d' % self.pid) os.kill(self.pid, signal.SIGHUP) @@ -921,6 +927,9 @@ def on_keypress(self, widget, event): if groupsend == groupsend_type['all']: self.terminator.all_emit(self, 'key-press-event', event) + if self.terminator.tmux_control: + self.control.send_keypress(event, pane_id=self.pane_id) + return False def on_buttonpress(self, widget, event): @@ -1011,6 +1020,8 @@ def on_mousewheel(self, widget, event): elif event.direction == Gdk.ScrollDirection.DOWN or SMOOTH_SCROLL_DOWN: self.scroll_by_page(1) return True + if self.terminator.tmux_control: + return self.control.send_mousewheel(event, pane_id=self.pane_id) return False def popup_menu(self, widget, event=None): @@ -1315,8 +1326,20 @@ def do_deferred_on_vte_size_allocate(self, widget, allocation): self.on_vte_size_allocate(widget, allocation) def on_vte_size_allocate(self, widget, allocation): - self.titlebar.update_terminal_size(self.vte.get_column_count(), - self.vte.get_row_count()) + column_count = self.vte.get_column_count() + row_count = self.vte.get_row_count() + self.titlebar.update_terminal_size(column_count, row_count) + + if self.terminator.tmux_control: + # self.terminator.tmux_control.resize_pane(self.pane_id, row_count, column_count) + # FIXME: probably not the best place for this, update tmux client size to match the window geometry + window = self.terminator.get_windows()[0] + column_count, row_count = map(int, get_column_row_count(window)) + # dbg("{}::{}: {}x{}".format("NotificationsHandler", "list_panes_size_result", column_count, row_count)) + size_up_to_date = bool(column_count == self.terminator.tmux_control.width and row_count == self.terminator.tmux_control.height) + if not size_up_to_date: + self.terminator.tmux_control.refresh_client(column_count, row_count) + if self.config['geometry_hinting']: window = self.get_toplevel() window.deferred_set_rough_geometry_hints() @@ -1410,7 +1433,8 @@ def held_open(self, widget=None, respawn=False, debugserver=False): self.is_held_open = True self.titlebar.update() - def spawn_child(self, widget=None, respawn=False, debugserver=False): + def spawn_child(self, widget=None, respawn=False, debugserver=False, + orientation=None, active_pane_id=None): args = [] shell = None command = None @@ -1486,15 +1510,27 @@ def spawn_child(self, widget=None, respawn=False, debugserver=False): envv.append('TERMINATOR_DBUS_PATH=%s' % self.terminator.dbus_path) dbg('Forking shell: "%s" with args: %s' % (shell, args)) - args.insert(0, shell) - result, self.pid = self.vte.spawn_sync(Vte.PtyFlags.DEFAULT, - self.cwd, - args, - envv, - GLib.SpawnFlags.FILE_AND_ARGV_ZERO, - None, - None, - None) + if self.terminator.tmux_control: + if self.terminator.initial_layout: + pass + else: + command = ' '.join(args) + self.pane_id = str(util.make_uuid()) + self.control.run_command(command=command, + cwd=self.cwd, + marker=self.pane_id, + orientation=orientation, + pane_id=active_pane_id) + else: + args.insert(0, shell) + result, self.pid = self.vte.spawn_sync(Vte.PtyFlags.DEFAULT, + self.cwd, + args, + envv, + GLib.SpawnFlags.FILE_AND_ARGV_ZERO, + None, + None, + None) self.command = shell self.titlebar.update() @@ -1562,12 +1598,18 @@ def open_url(self, url, prepare=False): def paste_clipboard(self, primary=False): """Paste one of the two clipboards""" - for term in self.terminator.get_target_terms(self): - if primary: - term.vte.paste_primary() - else: - term.vte.paste_clipboard() - self.vte.grab_focus() + if self.terminator.tmux_control: + def callback(_, content): + content = quote(content.replace('\n', '\r')) + self.control.send_quoted_content(content, self.pane_id) + self.clipboard.request_text(callback) + else: + for term in self.terminator.get_target_terms(self): + if primary: + term.vte.paste_primary() + else: + term.vte.paste_clipboard() + self.vte.grab_focus() def feed(self, text): """Feed the supplied text to VTE""" @@ -1700,6 +1742,11 @@ def create_layout(self, layout): self.directory = layout['directory'] if 'uuid' in layout and layout['uuid'] != '': self.uuid = make_uuid(layout['uuid']) + if layout.has_key('tmux'): + tmux = layout['tmux'] + self.pane_id = tmux['pane_id'] + self.terminator.pane_id_to_terminal[self.pane_id] = self + self.control.initial_output(self.pane_id) def scroll_by_page(self, pages): """Scroll up or down in pages""" @@ -1828,12 +1875,16 @@ def key_move_tab_left(self): self.emit('move-tab', 'left') def key_toggle_zoom(self): + if self.terminator.tmux_control: + self.control.toggle_zoom(self.pane_id) if self.is_zoomed(): self.unzoom() else: self.maximise() def key_scaled_zoom(self): + if self.terminator.tmux_control: + self.control.toggle_zoom(self.pane_id, zoom=True) if self.is_zoomed(): self.unzoom() else: diff --git a/terminatorlib/terminator.py b/terminatorlib/terminator.py index 6cbf4d68d..c7735ef9c 100644 --- a/terminatorlib/terminator.py +++ b/terminatorlib/terminator.py @@ -16,13 +16,14 @@ from .util import dbg, err, enumerate_descendants from .factory import Factory from .version import APP_NAME, APP_VERSION +import tmux.control +import tmux.notifications try: from gi.repository import GdkX11 except ImportError: dbg("could not import X11 gir module") - def eventkey2gdkevent(eventkey): # FIXME FOR GTK3: is there a simpler way of casting from specific EventKey to generic (union) GdkEvent? gdkevent = Gdk.Event.new(eventkey.type) gdkevent.key.window = eventkey.window @@ -67,6 +68,10 @@ class Terminator(Borg): cur_gtk_theme_name = None gtk_settings = None + tmux_control = None + pane_id_to_terminal = None + initial_layout = None + def __init__(self): """Class initialiser""" @@ -95,6 +100,13 @@ def prepare_attributes(self): self.style_providers = [] if not self.doing_layout: self.doing_layout = False + if not self.pid_cwd: + self.pid_cwd = get_pid_cwd() + if self.gnome_client is None: + self.attempt_gnome_client() + if self.pane_id_to_terminal is None: + self.pane_id_to_terminal = {} + self.connect_signals() def connect_signals(self): @@ -110,6 +122,16 @@ def set_origcwd(self, cwd): os.chdir(cwd) self.origcwd = cwd + def start_tmux(self, remote=None): + """Store the command line argument intended for tmux and start the process""" + if self.tmux_control is None: + handler = tmux.notifications.NotificationsHandler(self) + self.tmux_control = tmux.control.TmuxControl( + session_name='terminator', + notifications_handler=handler) + self.tmux_control.remote = remote + self.tmux_control.attach_session() + def set_dbus_data(self, dbus_service): """Store the DBus bus details, if they are available""" if dbus_service: @@ -201,6 +223,15 @@ def find_window_by_uuid(self, uuid): return window return None + def find_terminal_by_pane_id(self, pane_id): + """Search our terminals for one matching the supplied pane_id""" + dbg('searching self.terminals for: %s' % pane_id) + for terminal in self.terminals: + dbg('checking: %s (%s)' % (terminal.pane_id, terminal)) + if terminal.pane_id == pane_id: + return terminal + return None + def new_window(self, cwd=None, profile=None): """Create a window with a Terminal in it""" maker = Factory() @@ -218,19 +249,20 @@ def new_window(self, cwd=None, profile=None): def create_layout(self, layoutname): """Create all the parts necessary to satisfy the specified layout""" - layout = None + layout = copy.deepcopy(self.initial_layout) objects = {} self.doing_layout = True self.last_active_window = None self.prelayout_windows = self.windows[:] - layout = copy.deepcopy(self.config.layout_get_config(layoutname)) if not layout: - # User specified a non-existent layout. default to one Terminal - err('layout %s not defined' % layout) - self.new_window() - return + layout = copy.deepcopy(self.config.layout_get_config(layoutname)) + if not layout: + # User specified a non-existent layout. default to one Terminal + err('layout %s not defined' % layout) + self.new_window() + return # Wind the flat objects into a hierarchy hierarchy = {} diff --git a/terminatorlib/tmux/__init__.py b/terminatorlib/tmux/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/terminatorlib/tmux/control.py b/terminatorlib/tmux/control.py new file mode 100644 index 000000000..599f4f32e --- /dev/null +++ b/terminatorlib/tmux/control.py @@ -0,0 +1,306 @@ +import threading +import subprocess +import Queue + +from pipes import quote +from gi.repository import Gtk, Gdk + +from terminatorlib.tmux import notifications +from terminatorlib.util import dbg + +ESCAPE_CODE = '\033' +TMUX_BINARY = 'tmux' + +def esc(seq): + return '{}{}'.format(ESCAPE_CODE, seq) + + +KEY_MAPPINGS = { + Gdk.KEY_BackSpace: '\b', + Gdk.KEY_Tab: '\t', + Gdk.KEY_Insert: esc('[2~'), + Gdk.KEY_Delete: esc('[3~'), + Gdk.KEY_Page_Up: esc('[5~'), + Gdk.KEY_Page_Down: esc('[6~'), + Gdk.KEY_Home: esc('[1~'), + Gdk.KEY_End: esc('[4~'), + Gdk.KEY_Up: esc('[A'), + Gdk.KEY_Down: esc('[B'), + Gdk.KEY_Right: esc('[C'), + Gdk.KEY_Left: esc('[D'), +} +ARROW_KEYS = { + Gdk.KEY_Up, + Gdk.KEY_Down, + Gdk.KEY_Left, + Gdk.KEY_Right +} +MOUSE_WHEEL = { + # TODO: make it configurable, e.g. like better-mouse-mode plugin + Gdk.ScrollDirection.UP: "C-y C-y C-y", + Gdk.ScrollDirection.DOWN: "C-e C-e C-e", +} + +# TODO: implement ssh connection using paramiko +class TmuxControl(object): + + def __init__(self, session_name, notifications_handler): + self.session_name = session_name + self.notifications_handler = notifications_handler + self.tmux = None + self.output = None + self.input = None + self.consumer = None + self.width = None + self.height = None + self.remote = None + self.alternate_on = False + self.is_zoomed = False + self.requests = Queue.Queue() + + def reset(self): + self.tmux = self.input = self.output = self.width = self.height = None + + def remote_connect(self, command): + if self.tmux: + dbg("Already connected.") + return + popen_command = "ssh " + self.remote + self.tmux = subprocess.Popen(popen_command, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, shell=True) + self.input = self.tmux.stdin + self.output = self.tmux.stdout + + self.run_remote_command(command) + + def run_remote_command(self, popen_command): + popen_command = map(quote, popen_command) + command = " ".join(popen_command) + if not self.input: + dbg('No tmux connection. [command={}]'.format(command)) + else: + try: + self.input.write('exec {}\n'.format(command)) + except IOError: + dbg("Tmux server has gone away.") + return + + def run_command(self, command, marker, cwd=None, orientation=None, + pane_id=None): + if self.input: + if orientation: + self.split_window(cwd=cwd, orientation=orientation, + pane_id=pane_id, command=command, + marker=marker) + else: + self.new_window(cwd=cwd, command=command, marker=marker) + else: + self.new_session(cwd=cwd, command=command, marker=marker) + + def split_window(self, cwd, orientation, pane_id, + command=None, marker=''): + orientation = '-h' if orientation == 'horizontal' else '-v' + tmux_command = 'split-window {} -t {} -P -F "#D {}"'.format( + orientation, pane_id, marker) + if cwd: + tmux_command += ' -c "{}"'.format(cwd) + if command: + tmux_command += ' "{}"'.format(command) + + self._run_command(tmux_command, + callback=self.notifications_handler.pane_id_result) + + def new_window(self, cwd=None, command=None, marker=''): + tmux_command = 'new-window -P -F "#D {}"'.format(marker) + if cwd: + tmux_command += ' -c "{}"'.format(cwd) + if command: + tmux_command += ' "{}"'.format(command) + + self._run_command(tmux_command, + callback=self.notifications_handler.pane_id_result) + + def attach_session(self): + popen_command = [TMUX_BINARY, '-2', '-C', 'attach-session', + '-t', self.session_name] + if self.remote: + self.remote_connect(popen_command) + if not self.tmux: + self.tmux = subprocess.Popen(popen_command, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE) + self.input = self.tmux.stdin + self.output = self.tmux.stdout + self.requests.put(notifications.noop) + self.start_notifications_consumer() + self.initial_layout() + + def new_session(self, cwd=None, command=None, marker=''): + popen_command = [TMUX_BINARY, '-2', '-C', 'new-session', '-s', self.session_name, + '-P', '-F', '#D {}'.format(marker)] + if cwd and not self.remote: + popen_command += ['-c', cwd] + if command: + popen_command.append(command) + if self.remote: + self.remote_connect(popen_command) + if not self.tmux: + self.tmux = subprocess.Popen(popen_command, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE) + self.input = self.tmux.stdin + self.output = self.tmux.stdout + # starting a new session, delete any old requests we may have + # in the queue (e.g. those added while trying to attach to + # a nonexistant session) + with self.requests.mutex: + self.requests.queue.clear() + + self.requests.put(self.notifications_handler.pane_id_result) + self.start_notifications_consumer() + + def refresh_client(self, width, height): + dbg('{}::{}: {}x{}'.format("TmuxControl", "refresh_client", width, height)) + self.width = width + self.height = height + self._run_command('refresh-client -C {},{}'.format(width, height)) + + def garbage_collect_panes(self): + self._run_command('list-panes -s -t {} -F "#D {}"'.format( + self.session_name, '#{pane_pid}'), + callback=self.notifications_handler.garbage_collect_panes_result) + + def initial_layout(self): + self._run_command( + 'list-windows -t {} -F "#{{window_layout}}"' + .format(self.session_name), + callback=self.notifications_handler.initial_layout_result) + + def initial_output(self, pane_id): + self._run_command( + 'capture-pane -J -p -t {} -eC -S - -E -'.format(pane_id), + callback=self.notifications_handler.initial_output_result_callback( + pane_id)) + + def toggle_zoom(self, pane_id, zoom=False): + self.is_zoomed = not self.is_zoomed + if not zoom: + self._run_command('resize-pane -Z -x {} -y {} -t {}'.format(self.width, self.height, pane_id)) + + def send_keypress(self, event, pane_id): + keyval = event.keyval + state = event.state + + if keyval in KEY_MAPPINGS: + key = KEY_MAPPINGS[keyval] + if keyval in ARROW_KEYS and state & Gdk.ModifierType.CONTROL_MASK: + key = '{}1;5{}'.format(key[:2], key[2:]) + else: + key = event.string + + if state & Gdk.ModifierType.MOD1_MASK: + # Hack to have CTRL+SHIFT+Alt PageUp/PageDown/Home/End + # work without these silly [... escaped characters + if state & (Gdk.ModifierType.CONTROL_MASK | + Gdk.ModifierType.SHIFT_MASK): + return + else: + key = esc(key) + + if key == ';': + key = '\\;' + + self.send_content(key, pane_id) + + # Handle mouse scrolling events if the alternate_screen is visible + # otherwise let Terminator handle all the mouse behavior + def send_mousewheel(self, event, pane_id): + SMOOTH_SCROLL_UP = event.direction == Gdk.ScrollDirection.SMOOTH and event.delta_y <= 0. + SMOOTH_SCROLL_DOWN = event.direction == Gdk.ScrollDirection.SMOOTH and event.delta_y > 0. + if SMOOTH_SCROLL_UP: + wheel = MOUSE_WHEEL[Gdk.ScrollDirection.UP] + elif SMOOTH_SCROLL_DOWN: + wheel = MOUSE_WHEEL[Gdk.ScrollDirection.DOWN] + else: + wheel = MOUSE_WHEEL[event.direction] + + if self.alternate_on: + self._run_command("send-keys -t {} {}".format(pane_id, wheel)) + return True + return False + + def send_content(self, content, pane_id): + key_name_lookup = "-l" if ESCAPE_CODE in content else "" + quote = "'" if "'" not in content else '"' + self._run_command("send-keys -t {} {} -- {}{}{}".format( + pane_id, key_name_lookup, quote, content, quote)) + + def send_quoted_content(self, content, pane_id): + key_name_lookup = "-l" if ESCAPE_CODE in content else "" + self._run_command("send-keys -t {} {} -- {}".format( + pane_id, key_name_lookup, content)) + + def _run_command(self, command, callback=None): + if not self.input: + dbg('No tmux connection. [command={}]'.format(command)) + else: + try: + self.input.write('{}\n'.format(command)) + except IOError: + dbg("Tmux server has gone away.") + return + callback = callback or notifications.noop + self.requests.put(callback) + + @staticmethod + def kill_server(): + command = [TMUX_BINARY, 'kill-session', '-t', 'terminator'] + subprocess.call(command) + + def start_notifications_consumer(self): + self.consumer = threading.Thread(target=self.consume_notifications) + self.consumer.daemon = True + self.consumer.start() + + def consume_notifications(self): + handler = self.notifications_handler + while True: + try: + if self.tmux.poll() is not None: + break + except AttributeError as e: + dbg("Tmux control instance was reset.") + return + line = self.output.readline()[:-1] + if not line: + continue + line = line[1:].split(' ') + marker = line[0] + line = line[1:] + # skip MOTD, anything that isn't coming from tmux control mode + try: + notification = notifications.notifications_mappings[marker]() + except KeyError: + dbg("Discarding invalid output from the control terminal.") + continue + notification.consume(line, self.output) + handler.handle(notification) + handler.terminate() + + def display_pane_tty(self, pane_id): + tmux_command = 'display -pt "{}" "#D {}"'.format( + pane_id, "#{pane_tty}") + + self._run_command(tmux_command, + callback=self.notifications_handler.pane_tty_result) + + def resize_pane(self, pane_id, rows, cols): + if self.is_zoomed: + # if the pane is zoomed, there is no need for tmux to + # change the current layout + return + tmux_command = 'resize-pane -t "{}" -x {} -y {}'.format( + pane_id, cols, rows) + + self._run_command(tmux_command) diff --git a/terminatorlib/tmux/layout.py b/terminatorlib/tmux/layout.py new file mode 100644 index 000000000..56edb00ce --- /dev/null +++ b/terminatorlib/tmux/layout.py @@ -0,0 +1,260 @@ +from pyparsing import * + +class LayoutParser(): + """BNF representation for a Tmux Layout + :: + ; + :: ( | ) ? ; + :: {4} ; + :: + ; + :: ; + :: ; + :: "x" ; + :: "{" | "[" ; + :: "}" | "]" ; + :: + ; + :: + ; + :: "0" | ... | "9" ; + :: | "a" | ... | "f" ; + :: "," ; + """ + layout_parser = None + + def __init__(self): + decimal = Word(nums) + + comma = Suppress(Literal(',')) + start_token = Literal('{') | Literal('[') + end_token = Suppress(Literal('}') | Literal(']')) + + layout_name = Suppress(Word(hexnums, min=4, max=4)) + size = decimal("width") + Suppress(Literal('x')) + decimal("height") + + preamble = size + comma + decimal("x") + comma + decimal("y") + pane = Group(preamble + comma + decimal("pane_id")) + element = Forward() # will be defined later + container = Group(preamble + start_token + OneOrMore(element) + end_token) + + element << (container | pane) + Optional(comma) + + self.layout_parser = layout_name + comma + OneOrMore(element) + + def parse(self, layout): + parsed = self.layout_parser.parseString(layout) + return parsed.asList() + +def parse_layout(layout): + """Apply our application logic to the parsed layout. + + Arguments: + layout -- Layout parsed by LayoutParser.parse(), + it is represented as a nested list; each nested + list has the following format: + [0] : width, + [1] : height, + [2] : position on x axis, + [3] : position on y axis, + [4] : '{' if the current element is a horizontal splits container, + '[' if the current element is a vertical splits container, + '%[0-9]+' if the current element is a pane + [5+] : if present, they are nested lists with the same structure + + """ + result = [] + + children = [] + for item in layout[5:]: + children.extend(parse_layout(item)) + + if layout[4] == '{': + result.append(Horizontal( + layout[0], + layout[1], + layout[2], + layout[3], + children + )) + + elif layout[4] == '[': + result.append(Vertical( + layout[0], + layout[1], + layout[2], + layout[3], + children + )) + else: + result.append(Pane( + layout[0], + layout[1], + layout[2], + layout[3], + "%{}".format(layout[4]) + )) + + return result + +def convert_to_terminator_layout(window_layouts): + assert len(window_layouts) > 0 + result = {} + pane_index = 0 + window_name = 'window0' + parent_name = window_name + result[window_name] = { + 'type': 'Window', + 'parent': '' + } + if len(window_layouts) > 1: + notebook_name = 'notebook0' + result[notebook_name] = { + 'type': 'Notebook', + 'parent': parent_name + } + parent_name = notebook_name + order = 0 + for window_layout in window_layouts: + converter = _get_converter(window_layout) + pane_index, order = converter( + result, parent_name, window_layout, pane_index, order) + return result + + +class Container(object): + + def __init__(self, width, height, x, y): + self.width = width + self.height = height + self.x = x + self.y = y + + def __str__(self): + return ( + '{}[width={}, height={}, x={}, y={}, {}]' + .format(self.__class__.__name__, + self.width, self.height, self.x, self.y, + self._child_str())) + + __repr__ = __str__ + + def _child_str(self): + raise NotImplementedError() + + +class Pane(Container): + + def __init__(self, width, height, x, y, pane_id): + super(Pane, self).__init__(width, height, x, y) + self.pane_id = pane_id + + def _child_str(self): + return 'pane_id={}'.format(self.pane_id) + + +class Vertical(Container): + + def __init__(self, width, height, x, y, children): + super(Vertical, self).__init__(width, height, x, y) + self.children = children + + def _child_str(self): + return 'children={}'.format(self.children) + + +class Horizontal(Container): + + def __init__(self, width, height, x, y, children): + super(Horizontal, self).__init__(width, height, x, y) + self.children = children + + def _child_str(self): + return 'children={}'.format(self.children) + + +def _covert_pane_to_terminal(result, parent_name, pane, pane_index, order): + assert isinstance(pane, Pane) + terminal = _convert(parent_name, 'Terminal', pane, order) + order += 1 + terminal['tmux']['pane_id'] = pane.pane_id + result['terminal{}'.format(pane.pane_id[1:])] = terminal + return pane_index, order + + +def _convert_vertical_to_vpane(result, parent_name, vertical_or_children, + pane_index, order): + return _convert_container_to_terminator_pane( + result, parent_name, vertical_or_children, pane_index, Vertical, + order) + + +def _convert_horizontal_to_hpane(result, parent_name, horizontal_or_children, + pane_index, order): + return _convert_container_to_terminator_pane( + result, parent_name, horizontal_or_children, pane_index, + Horizontal, order) + + +def _convert_container_to_terminator_pane(result, parent_name, + container_or_children, + pane_index, pane_type, + order): + terminator_type = 'VPaned' if issubclass(pane_type, Vertical) else 'HPaned' + if isinstance(container_or_children, pane_type): + container = container_or_children + pane = _convert(parent_name, terminator_type, container_or_children, + order) + order += 1 + children = container.children + else: + children = container_or_children + if len(children) == 1: + child = children[0] + child_converter = _get_converter(child) + return child_converter(result, parent_name, child, pane_index, + order) + pane = { + 'type': terminator_type, + 'parent': parent_name + } + pane_name = 'pane{}'.format(pane_index) + result[pane_name] = pane + parent_name = pane_name + pane_index += 1 + child1 = children[0] + child1_converter = _get_converter(child1) + pane_index, order = child1_converter(result, parent_name, child1, + pane_index, order) + pane_index, order = _convert_container_to_terminator_pane(result, + parent_name, + children[1:], + pane_index, + pane_type, + order) + return pane_index, order + + +converters = { + Pane: _covert_pane_to_terminal, + Vertical: _convert_vertical_to_vpane, + Horizontal: _convert_horizontal_to_hpane +} + + +def _get_converter(container): + try: + return converters[type(container)] + except KeyError: + raise ValueError('Illegal window layout: {}'.format(container)) + + +def _convert(parent_name, type_name, container, order): + assert isinstance(container, Container) + return { + 'type': type_name, + 'parent': parent_name, + 'order': order, + 'tmux': { + 'width': container.width, + 'height': container.height, + 'x': container.x, + 'y': container.y + } + } diff --git a/terminatorlib/tmux/notifications.py b/terminatorlib/tmux/notifications.py new file mode 100644 index 000000000..a1faceb93 --- /dev/null +++ b/terminatorlib/tmux/notifications.py @@ -0,0 +1,310 @@ +from gi.repository import GObject + +from terminatorlib.util import dbg +from terminatorlib.tmux import layout + +import string +ATTACH_ERROR_STRINGS = ["can't find session terminator", "no current session", "no sessions"] +ALTERNATE_SCREEN_ENTER_CODES = [ "\\033[?1049h" ] +ALTERNATE_SCREEN_EXIT_CODES = [ "\\033[?1049l" ] + +notifications_mappings = {} + + +def notification(cls): + notifications_mappings[cls.marker] = cls + return cls + + +class Notification(object): + + marker = 'undefined' + attributes = [] + + def consume(self, line, out): + pass + + def __str__(self): + attributes = ['{}="{}"'.format(attribute, getattr(self, attribute, '')) + for attribute in self.attributes] + return '{}[{}]'.format(self.marker, ', '.join(attributes)) + + +@notification +class Result(Notification): + + marker = 'begin' + attributes = ['begin_timestamp', 'code', 'result', 'end_timestamp', + 'error'] + + def consume(self, line, out): + timestamp, code, _ = line + self.begin_timestamp = timestamp + self.code = code + result = [] + line = out.readline()[:-1] + while not (line.startswith('%end') or line.startswith('%error')): + result.append(line) + line = out.readline()[:-1] + self.result = result + end, timestamp, code, _ = line.split(' ') + self.end_timestamp = timestamp + self.error = end == '%error' + + +@notification +class Exit(Notification): + + marker = 'exit' + attributes = ['reason'] + + def consume(self, line, *args): + self.reason = line[0] if line else None + + +@notification +class LayoutChange(Notification): + + marker = 'layout-change' + attributes = ['window_id', 'window_layout', 'window_visible_layout', + 'window_flags'] + + def consume(self, line, *args): + # attributes not present default to None + window_id, window_layout, window_visible_layout, window_flags = line + [None] * (len(self.attributes) - len(line)) + self.window_id = window_id + self.window_layout = window_layout + self.window_visible_layout = window_visible_layout + self.window_flags = window_flags + +@notification +class Output(Notification): + + marker = 'output' + attributes = ['pane_id', 'output'] + + def consume(self, line, *args): + pane_id = line[0] + output = ' '.join(line[1:]) + self.pane_id = pane_id + self.output = output + +@notification +class SessionChanged(Notification): + + marker = 'session-changed' + attributes = ['session_id', 'session_name'] + + def consume(self, line, *args): + session_id, session_name = line + self.session_id = session_id + self.session_name = session_name + + +@notification +class SessionRenamed(Notification): + + marker = 'session-renamed' + attributes = ['session_id', 'session_name'] + + def consume(self, line, *args): + session_id, session_name = line + self.session_id = session_id + self.session_name = session_name + + +@notification +class SessionsChanged(Notification): + + marker = 'sessions-changed' + attributes = [] + + +@notification +class UnlinkedWindowAdd(Notification): + + marker = 'unlinked-window-add' + attributes = ['window_id'] + + def consume(self, line, *args): + window_id, = line + self.window_id = window_id + + +@notification +class WindowAdd(Notification): + + marker = 'window-add' + attributes = ['window_id'] + + def consume(self, line, *args): + window_id, = line + self.window_id = window_id + + +@notification +class UnlinkedWindowClose(Notification): + + marker = 'unlinked-window-close' + attributes = ['window_id'] + + def consume(self, line, *args): + window_id, = line + self.window_id = window_id + + +@notification +class WindowClose(Notification): + + marker = 'window-close' + attributes = ['window_id'] + + def consume(self, line, *args): + window_id, = line + self.window_id = window_id + + +@notification +class UnlinkedWindowRenamed(Notification): + + marker = 'unlinked-window-renamed' + attributes = ['window_id', 'window_name'] + + def consume(self, line, *args): + window_id, window_name = line + self.window_id = window_id + self.window_name = window_name + + +@notification +class WindowRenamed(Notification): + + marker = 'window-renamed' + attributes = ['window_id', 'window_name'] + + def consume(self, line, *args): + window_id, window_name = line + self.window_id = window_id + self.window_name = window_name + + +class NotificationsHandler(object): + + def __init__(self, terminator): + self.terminator = terminator + self.layout_parser = layout.LayoutParser() + + def handle(self, notification): + try: + handler_method = getattr(self, 'handle_{}'.format( + notification.marker.replace('-', '_'))) + handler_method(notification) + except AttributeError: + pass + + def handle_begin(self, notification): + dbg('### {}'.format(notification)) + assert isinstance(notification, Result) + callback = self.terminator.tmux_control.requests.get() + if notification.error: + dbg('Request error: {}'.format(notification)) + if notification.result[0] in ATTACH_ERROR_STRINGS: + # if we got here it means that attaching to an existing session + # failed, invalidate the layout so the Terminator initialization + # can pick up from where we left off + self.terminator.initial_layout = {} + self.terminator.tmux_control.reset() + return + callback(notification.result) + + def handle_output(self, notification): + assert isinstance(notification, Output) + pane_id = notification.pane_id + output = notification.output + terminal = self.terminator.pane_id_to_terminal.get(pane_id) + if not terminal: + return + for code in ALTERNATE_SCREEN_ENTER_CODES: + if code in output: + self.terminator.tmux_control.alternate_on = True + for code in ALTERNATE_SCREEN_EXIT_CODES: + if code in output: + self.terminator.tmux_control.alternate_on = False + # NOTE: using neovim, enabling visual-bell and setting t_vb empty results in incorrect + # escape sequences (C-g) being printed in the neovim window; remove them until we can + # figure out the root cause + terminal.vte.feed(output.decode('string_escape').replace("\033g","")) + + def handle_layout_change(self, notification): + assert isinstance(notification, LayoutChange) + GObject.idle_add(self.terminator.tmux_control.garbage_collect_panes) + + def handle_window_close(self, notification): + assert isinstance(notification, WindowClose) + GObject.idle_add(self.terminator.tmux_control.garbage_collect_panes) + + def pane_id_result(self, result): + pane_id, marker = result[0].split(' ') + terminal = self.terminator.find_terminal_by_pane_id(marker) + terminal.pane_id = pane_id + self.terminator.pane_id_to_terminal[pane_id] = terminal + + # NOTE: UNUSED; if we ever end up needing this, create the tty property in + # the Terminal class first + def pane_tty_result(self, result): + dbg(result) + pane_id, pane_tty = result[0].split(' ') + # self.terminator.pane_id_to_terminal[pane_id].tty = pane_tty + + def garbage_collect_panes_result(self, result): + pane_id_to_terminal = self.terminator.pane_id_to_terminal + removed_pane_ids = pane_id_to_terminal.keys() + + for line in result: + pane_id, pane_pid = line.split(' ') + try: + removed_pane_ids.remove(pane_id) + pane_id_to_terminal[pane_id].pid = pane_pid + except ValueError: + dbg("Pane already reaped, keep going.") + continue + + if removed_pane_ids: + def callback(): + for pane_id in removed_pane_ids: + terminal = pane_id_to_terminal.pop(pane_id, None) + if terminal: + terminal.close() + return False + GObject.idle_add(callback) + + def initial_layout_result(self, result): + window_layouts = [] + for line in result: + window_layout = line.strip() + window_layouts.extend(layout.parse_layout(self.layout_parser.parse(window_layout)[0])) + # window_layouts.append(layout.parse_layout(window_layout)) + terminator_layout = layout.convert_to_terminator_layout( + window_layouts) + import pprint + dbg(pprint.pformat(terminator_layout)) + self.terminator.initial_layout = terminator_layout + + def initial_output_result_callback(self, pane_id): + def result_callback(result): + terminal = self.terminator.pane_id_to_terminal.get(pane_id) + if not terminal: + return + output = '\r\n'.join(l for l in result if l) + terminal.vte.feed(output.decode('string_escape')) + return result_callback + + def terminate(self): + def callback(): + for window in self.terminator.windows: + window.emit('destroy') + GObject.idle_add(callback) + + +def noop(result): + pass diff --git a/terminatorlib/util.py b/terminatorlib/util.py index 769343a56..1c8cda331 100644 --- a/terminatorlib/util.py +++ b/terminatorlib/util.py @@ -35,7 +35,7 @@ sys.exit(1) # set this to true to enable debugging output -DEBUG = False +DEBUG = True # set this to true to additionally list filenames in debugging DEBUGFILES = False # list of classes to show debugging for. empty list means show all classes @@ -320,6 +320,43 @@ def enumerate_descendants(parent): len(terminals), parent)) return(containers, terminals) +def get_column_row_count(window): + column_sum = 0 + row_sum = 0 + + base_x = base_y = None + + # NOTE: on Wayland, we cannot assume that the coordinate system + # for our application starts at 0x0, so we try to guess our + # current baseline at runtime + if display_manager() == 'WAYLAND' and not base_x: + base_x, base_y = get_wayland_baseline(window) + else: + base_x = base_y = 0 + + terminals = window.get_visible_terminals() + for terminal in terminals: + rect = terminal.get_allocation() + if rect.x <= base_x: + cols, rows = terminal.get_size() + row_sum = row_sum + int(rows) + if rect.y <= base_y: + cols, rows = terminal.get_size() + column_sum = column_sum + int(cols) + + return (column_sum, row_sum) + +def get_wayland_baseline(window): + terminals = window.get_visible_terminals() + + base_x = base_y = sys.maxint + for terminal in terminals: + rect = terminal.get_allocation() + base_x = min(base_x, rect.x) + base_y = min(base_y, rect.y) + + return (base_x, base_y) + def make_uuid(str_uuid=None): """Generate a UUID for an object""" if str_uuid: diff --git a/terminatorlib/window.py b/terminatorlib/window.py index fb01845aa..7efda19f5 100644 --- a/terminatorlib/window.py +++ b/terminatorlib/window.py @@ -473,13 +473,15 @@ def split_axis(self, widget, vertical=True, cwd=None, sibling=None, widgetfirst= container = maker.make('HPaned') self.set_pos_by_ratio = True - if not sibling: sibling = maker.make('Terminal') sibling.set_cwd(cwd) + # TODO (dank): is widget ever not a Terminal? if self.config['always_split_with_profile']: sibling.force_set_profile(None, widget.get_profile()) - sibling.spawn_child() + sibling.spawn_child( + orientation='vertical' if vertical else 'horizontal', + active_pane_id=getattr(widget, 'pane_id', None)) if widget.group and self.config['split_to_group']: sibling.set_group(None, widget.group) elif self.config['always_split_with_profile']: @@ -630,9 +632,9 @@ def set_rough_geometry_hints(self): return terminals = self.get_visible_terminals() + column_sum = 0 row_sum = 0 - for terminal in terminals: rect = terminal.get_allocation() if rect.x == 0: diff --git a/tests/test_tmux.py b/tests/test_tmux.py new file mode 100644 index 000000000..1dac378f4 --- /dev/null +++ b/tests/test_tmux.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python2 + +import os +import sys, os.path +sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), ".."))) + +import unittest +from terminatorlib.tmux import notifications + +class NotificationsTests(unittest.TestCase): + + def test_layout_changed_parsing(self): + layouts = [ + 'sum,80x24,0,0,0', + 'sum,80x24,0,0[80x12,0,0,0,80x5,0,13,1,80x2,0,19,2,80x2,0,22,3]', + 'sum,80x24,0,0{40x24,0,0,0,19x24,41,0,1,9x24,61,0,2,4x24,71,0,3,4x24,76,0,4}', + 'sum,80x24,0,0[80x12,0,0,0,80x5,0,13,1,80x5,0,19{40x5,0,19,2,19x5,41,19,3,9x5,61,19,4,4x5,71,19,5,4x5,76,19,6}]' + ] + for layout in layouts: + notification = notifications.LayoutChange() + notification.consume(['', layout]) + print notification.window_layout + + +def main(): + unittest.main() + +if __name__ == '__main__': + main() From d528ea3132601ec333b28eae83b39c24fa42e90b Mon Sep 17 00:00:00 2001 From: Mario Manno Date: Sun, 31 Jan 2021 12:45:16 +0100 Subject: [PATCH 02/29] wip: fix tmux mode imports, python3 strings/bytes, -M already used --- TMUX.md | 4 ++-- terminatorlib/optionparse.py | 2 +- terminatorlib/terminal.py | 2 +- terminatorlib/terminator.py | 18 +++++++----------- terminatorlib/tmux/control.py | 11 ++++++----- terminatorlib/tmux/notifications.py | 4 ++-- 6 files changed, 19 insertions(+), 22 deletions(-) diff --git a/TMUX.md b/TMUX.md index 1af0f1f1a..955e5423d 100644 --- a/TMUX.md +++ b/TMUX.md @@ -4,10 +4,10 @@ Terminator support the [tmux control mode](http://man7.org/linux/man-pages/man1/ Remote SSH example, starts tmux on remote host and displays tabs and splits in terminator: ``` -terminator -M --remote example.org +terminator -t --remote example.org ``` Local session: ``` -terminator -M +terminator -t ``` diff --git a/terminatorlib/optionparse.py b/terminatorlib/optionparse.py index 3e67d132f..f745b3cc6 100644 --- a/terminatorlib/optionparse.py +++ b/terminatorlib/optionparse.py @@ -57,7 +57,7 @@ def parse_options(): dest='borderless', help=_('Disable window borders')) parser.add_option('-H', '--hidden', action='store_true', dest='hidden', help=_('Hide the window at startup')) - parser.add_option('-M', '--tmux', action='store_true', + parser.add_option('-t', '--tmux', action='store_true', dest='tmux', help=_('Enable tmux integration')) parser.add_option('-T', '--title', dest='forcedtitle', help=_('Specify a title for the window')) diff --git a/terminatorlib/terminal.py b/terminatorlib/terminal.py index 9273e5db3..b21c751c5 100644 --- a/terminatorlib/terminal.py +++ b/terminatorlib/terminal.py @@ -1742,7 +1742,7 @@ def create_layout(self, layout): self.directory = layout['directory'] if 'uuid' in layout and layout['uuid'] != '': self.uuid = make_uuid(layout['uuid']) - if layout.has_key('tmux'): + if 'tmux' in layout and layout['tmux'] != '': tmux = layout['tmux'] self.pane_id = tmux['pane_id'] self.terminator.pane_id_to_terminal[self.pane_id] = self diff --git a/terminatorlib/terminator.py b/terminatorlib/terminator.py index c7735ef9c..ba59096bd 100644 --- a/terminatorlib/terminator.py +++ b/terminatorlib/terminator.py @@ -16,8 +16,8 @@ from .util import dbg, err, enumerate_descendants from .factory import Factory from .version import APP_NAME, APP_VERSION -import tmux.control -import tmux.notifications +from .tmux import notifications +from .tmux import control try: from gi.repository import GdkX11 @@ -100,10 +100,6 @@ def prepare_attributes(self): self.style_providers = [] if not self.doing_layout: self.doing_layout = False - if not self.pid_cwd: - self.pid_cwd = get_pid_cwd() - if self.gnome_client is None: - self.attempt_gnome_client() if self.pane_id_to_terminal is None: self.pane_id_to_terminal = {} @@ -125,8 +121,8 @@ def set_origcwd(self, cwd): def start_tmux(self, remote=None): """Store the command line argument intended for tmux and start the process""" if self.tmux_control is None: - handler = tmux.notifications.NotificationsHandler(self) - self.tmux_control = tmux.control.TmuxControl( + handler = notifications.NotificationsHandler(self) + self.tmux_control = control.TmuxControl( session_name='terminator', notifications_handler=handler) self.tmux_control.remote = remote @@ -522,7 +518,7 @@ def reconfigure(self): css += """ .terminator-terminal-window separator { min-height: %spx; - min-width: %spx; + min-width: %spx; } """ % (self.config['handle_size'],self.config['handle_size']) style_provider = Gtk.CssProvider() @@ -668,11 +664,11 @@ def describe_layout(self): def zoom_in_all(self): for term in self.terminals: term.zoom_in() - + def zoom_out_all(self): for term in self.terminals: term.zoom_out() - + def zoom_orig_all(self): for term in self.terminals: term.zoom_orig() diff --git a/terminatorlib/tmux/control.py b/terminatorlib/tmux/control.py index 599f4f32e..1fa12d0e9 100644 --- a/terminatorlib/tmux/control.py +++ b/terminatorlib/tmux/control.py @@ -1,6 +1,7 @@ import threading import subprocess -import Queue + +from multiprocessing import Queue from pipes import quote from gi.repository import Gtk, Gdk @@ -56,7 +57,7 @@ def __init__(self, session_name, notifications_handler): self.remote = None self.alternate_on = False self.is_zoomed = False - self.requests = Queue.Queue() + self.requests = Queue() def reset(self): self.tmux = self.input = self.output = self.width = self.height = None @@ -81,7 +82,7 @@ def run_remote_command(self, popen_command): dbg('No tmux connection. [command={}]'.format(command)) else: try: - self.input.write('exec {}\n'.format(command)) + self.input.write('exec {}\n'.format(command).encode()) except IOError: dbg("Tmux server has gone away.") return @@ -246,7 +247,7 @@ def _run_command(self, command, callback=None): dbg('No tmux connection. [command={}]'.format(command)) else: try: - self.input.write('{}\n'.format(command)) + self.input.write('{}\n'.format(command).encode()) except IOError: dbg("Tmux server has gone away.") return @@ -275,7 +276,7 @@ def consume_notifications(self): line = self.output.readline()[:-1] if not line: continue - line = line[1:].split(' ') + line = line[1:].decode().split(' ') marker = line[0] line = line[1:] # skip MOTD, anything that isn't coming from tmux control mode diff --git a/terminatorlib/tmux/notifications.py b/terminatorlib/tmux/notifications.py index a1faceb93..821e4ad66 100644 --- a/terminatorlib/tmux/notifications.py +++ b/terminatorlib/tmux/notifications.py @@ -43,11 +43,11 @@ def consume(self, line, out): self.code = code result = [] line = out.readline()[:-1] - while not (line.startswith('%end') or line.startswith('%error')): + while not (line.startswith(b'%end') or line.startswith(b'%error')): result.append(line) line = out.readline()[:-1] self.result = result - end, timestamp, code, _ = line.split(' ') + end, timestamp, code, _ = line.split(b' ') self.end_timestamp = timestamp self.error = end == '%error' From 52047e7dbfd830569500d4e4b023cc741beec03a Mon Sep 17 00:00:00 2001 From: Jack Gaino Date: Sat, 20 Mar 2021 22:10:47 -0400 Subject: [PATCH 03/29] Ignore virtual environment folder --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index d7a0e2637..a91f7d1f1 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,5 @@ terminatorlib/meliae .intltool* data/terminator.appdata.xml data/terminator.desktop + +venv/ From 279fc1ed27b5fd73ca65d52ec52e6ce6162eeddf Mon Sep 17 00:00:00 2001 From: Jack Gaino Date: Sat, 20 Mar 2021 22:12:12 -0400 Subject: [PATCH 04/29] Disable buffering so stdin is immediately sent --- terminatorlib/tmux/control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminatorlib/tmux/control.py b/terminatorlib/tmux/control.py index 1fa12d0e9..210bed963 100644 --- a/terminatorlib/tmux/control.py +++ b/terminatorlib/tmux/control.py @@ -130,7 +130,7 @@ def attach_session(self): if not self.tmux: self.tmux = subprocess.Popen(popen_command, stdout=subprocess.PIPE, - stdin=subprocess.PIPE) + stdin=subprocess.PIPE, bufsize=0) self.input = self.tmux.stdin self.output = self.tmux.stdout self.requests.put(notifications.noop) From 6d627117269108c40177e68f80fd05068fedf9e5 Mon Sep 17 00:00:00 2001 From: Jack Gaino Date: Sat, 20 Mar 2021 22:12:45 -0400 Subject: [PATCH 05/29] Fix emptying out of queue (hack) --- terminatorlib/tmux/control.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/terminatorlib/tmux/control.py b/terminatorlib/tmux/control.py index 210bed963..9b8b861ca 100644 --- a/terminatorlib/tmux/control.py +++ b/terminatorlib/tmux/control.py @@ -155,8 +155,8 @@ def new_session(self, cwd=None, command=None, marker=''): # starting a new session, delete any old requests we may have # in the queue (e.g. those added while trying to attach to # a nonexistant session) - with self.requests.mutex: - self.requests.queue.clear() + while not self.requests.empty(): + self.requests.get(timeout=1) self.requests.put(self.notifications_handler.pane_id_result) self.start_notifications_consumer() From 294ff9265539edf37d1385a2198464414a9a4672 Mon Sep 17 00:00:00 2001 From: Jack Gaino Date: Sat, 20 Mar 2021 22:15:15 -0400 Subject: [PATCH 06/29] Use callback name instead of function references multiprocessing.Queue actually has to pickle everything that goes through it. This is not possible in all cases, so the callbacks were not being added. This commit switches to an implementation that can instead find the needed function by name. --- terminatorlib/tmux/control.py | 16 ++++++++-------- terminatorlib/tmux/notifications.py | 20 +++++++++++++++++--- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/terminatorlib/tmux/control.py b/terminatorlib/tmux/control.py index 9b8b861ca..e5c081e02 100644 --- a/terminatorlib/tmux/control.py +++ b/terminatorlib/tmux/control.py @@ -110,7 +110,7 @@ def split_window(self, cwd, orientation, pane_id, tmux_command += ' "{}"'.format(command) self._run_command(tmux_command, - callback=self.notifications_handler.pane_id_result) + callback=('pane_id_result',)) def new_window(self, cwd=None, command=None, marker=''): tmux_command = 'new-window -P -F "#D {}"'.format(marker) @@ -120,7 +120,7 @@ def new_window(self, cwd=None, command=None, marker=''): tmux_command += ' "{}"'.format(command) self._run_command(tmux_command, - callback=self.notifications_handler.pane_id_result) + callback=('pane_id_result',)) def attach_session(self): popen_command = [TMUX_BINARY, '-2', '-C', 'attach-session', @@ -158,7 +158,7 @@ def new_session(self, cwd=None, command=None, marker=''): while not self.requests.empty(): self.requests.get(timeout=1) - self.requests.put(self.notifications_handler.pane_id_result) + self.requests.put(('pane_id_result',)) self.start_notifications_consumer() def refresh_client(self, width, height): @@ -170,19 +170,19 @@ def refresh_client(self, width, height): def garbage_collect_panes(self): self._run_command('list-panes -s -t {} -F "#D {}"'.format( self.session_name, '#{pane_pid}'), - callback=self.notifications_handler.garbage_collect_panes_result) + callback=('garbage_collect_panes_result',)) def initial_layout(self): self._run_command( 'list-windows -t {} -F "#{{window_layout}}"' .format(self.session_name), - callback=self.notifications_handler.initial_layout_result) + callback=('initial_layout_result',)) def initial_output(self, pane_id): self._run_command( 'capture-pane -J -p -t {} -eC -S - -E -'.format(pane_id), - callback=self.notifications_handler.initial_output_result_callback( - pane_id)) + callback=('result_callback', pane_id) + ) def toggle_zoom(self, pane_id, zoom=False): self.is_zoomed = not self.is_zoomed @@ -294,7 +294,7 @@ def display_pane_tty(self, pane_id): pane_id, "#{pane_tty}") self._run_command(tmux_command, - callback=self.notifications_handler.pane_tty_result) + callback=('pane_tty_result',)) def resize_pane(self, pane_id, rows, cols): if self.is_zoomed: diff --git a/terminatorlib/tmux/notifications.py b/terminatorlib/tmux/notifications.py index 821e4ad66..cd6590045 100644 --- a/terminatorlib/tmux/notifications.py +++ b/terminatorlib/tmux/notifications.py @@ -215,7 +215,13 @@ def handle_begin(self, notification): self.terminator.initial_layout = {} self.terminator.tmux_control.reset() return - callback(notification.result) + if isinstance(callback, tuple): + if len(callback) > 1: + self.__getattribute__(callback[0])(notification.result, *callback[1:]) + else: + self.__getattribute__(callback[0])(notification.result) + elif callable(callback): + callback(notification.result) def handle_output(self, notification): assert isinstance(notification, Output) @@ -290,6 +296,14 @@ def initial_layout_result(self, result): dbg(pprint.pformat(terminator_layout)) self.terminator.initial_layout = terminator_layout + def result_callback(self, result, pane_id): + terminal = self.terminator.pane_id_to_terminal.get(pane_id) + if not terminal: + return + output = b'\r\n'.join(l for l in result if l) + dbg(output) + terminal.vte.feed(output.decode('unicode-escape').encode('latin-1')) + def initial_output_result_callback(self, pane_id): def result_callback(result): terminal = self.terminator.pane_id_to_terminal.get(pane_id) @@ -306,5 +320,5 @@ def callback(): GObject.idle_add(callback) -def noop(result): - pass +def noop(*args): + dbg('passed on notification: {}'.format(args)) From 753b9461e4a2baffce8a7fe65f8de3d28be6208c Mon Sep 17 00:00:00 2001 From: Jack Gaino Date: Sat, 20 Mar 2021 22:17:18 -0400 Subject: [PATCH 07/29] Import get_column_row_count --- terminatorlib/terminal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminatorlib/terminal.py b/terminatorlib/terminal.py index b21c751c5..13b1fa804 100644 --- a/terminatorlib/terminal.py +++ b/terminatorlib/terminal.py @@ -16,7 +16,7 @@ except ImportError: from urllib import unquote as urlunquote -from .util import dbg, err, spawn_new_terminator, make_uuid, manual_lookup, display_manager +from .util import dbg, err, spawn_new_terminator, make_uuid, manual_lookup, display_manager, get_column_row_count from . import util from .config import Config from .cwd import get_pid_cwd From 473cac5fa8d41ae249b4b293d67c25d31b040380 Mon Sep 17 00:00:00 2001 From: Jack Gaino Date: Sat, 20 Mar 2021 22:19:10 -0400 Subject: [PATCH 08/29] Use bytes everywhere for now, we'll do some proper decoding where necessary --- terminatorlib/tmux/control.py | 7 ++-- terminatorlib/tmux/notifications.py | 53 ++++++++++++++++++----------- 2 files changed, 37 insertions(+), 23 deletions(-) diff --git a/terminatorlib/tmux/control.py b/terminatorlib/tmux/control.py index e5c081e02..66f7588e5 100644 --- a/terminatorlib/tmux/control.py +++ b/terminatorlib/tmux/control.py @@ -276,9 +276,10 @@ def consume_notifications(self): line = self.output.readline()[:-1] if not line: continue - line = line[1:].decode().split(' ') - marker = line[0] - line = line[1:] + dbg('=>>>>> LINE RECEIVED: {}'.format(line)) + line = line[1:].split(b' ', 1) + marker = line[0].decode() + line = line[1] # skip MOTD, anything that isn't coming from tmux control mode try: notification = notifications.notifications_mappings[marker]() diff --git a/terminatorlib/tmux/notifications.py b/terminatorlib/tmux/notifications.py index cd6590045..6d205c6bf 100644 --- a/terminatorlib/tmux/notifications.py +++ b/terminatorlib/tmux/notifications.py @@ -4,9 +4,9 @@ from terminatorlib.tmux import layout import string -ATTACH_ERROR_STRINGS = ["can't find session terminator", "no current session", "no sessions"] -ALTERNATE_SCREEN_ENTER_CODES = [ "\\033[?1049h" ] -ALTERNATE_SCREEN_EXIT_CODES = [ "\\033[?1049l" ] +ATTACH_ERROR_STRINGS = [b"can't find session terminator", b"no current session", b"no sessions"] +ALTERNATE_SCREEN_ENTER_CODES = [ b"\\033[?1049h" ] +ALTERNATE_SCREEN_EXIT_CODES = [ b"\\033[?1049l" ] notifications_mappings = {} @@ -38,7 +38,7 @@ class Result(Notification): 'error'] def consume(self, line, out): - timestamp, code, _ = line + timestamp, code, _ = line.split(b' ') self.begin_timestamp = timestamp self.code = code result = [] @@ -49,7 +49,7 @@ def consume(self, line, out): self.result = result end, timestamp, code, _ = line.split(b' ') self.end_timestamp = timestamp - self.error = end == '%error' + self.error = end == b'%error' @notification @@ -71,7 +71,8 @@ class LayoutChange(Notification): def consume(self, line, *args): # attributes not present default to None - window_id, window_layout, window_visible_layout, window_flags = line + [None] * (len(self.attributes) - len(line)) + line_items = line.split(b' ') + window_id, window_layout, window_visible_layout, window_flags = line_items + [None] * (len(self.attributes) - len(line_items)) self.window_id = window_id self.window_layout = window_layout self.window_visible_layout = window_visible_layout @@ -84,8 +85,9 @@ class Output(Notification): attributes = ['pane_id', 'output'] def consume(self, line, *args): - pane_id = line[0] - output = ' '.join(line[1:]) + # pane_id = line[0] + # output = ' '.join(line[1:]) + pane_id, output = line.split(b' ', 1) self.pane_id = pane_id self.output = output @@ -96,7 +98,7 @@ class SessionChanged(Notification): attributes = ['session_id', 'session_name'] def consume(self, line, *args): - session_id, session_name = line + session_id, session_name = line.split(b' ') self.session_id = session_id self.session_name = session_name @@ -108,7 +110,7 @@ class SessionRenamed(Notification): attributes = ['session_id', 'session_name'] def consume(self, line, *args): - session_id, session_name = line + session_id, session_name = line.split(b' ') self.session_id = session_id self.session_name = session_name @@ -127,7 +129,7 @@ class UnlinkedWindowAdd(Notification): attributes = ['window_id'] def consume(self, line, *args): - window_id, = line + window_id, = line.split(b' ') self.window_id = window_id @@ -138,7 +140,7 @@ class WindowAdd(Notification): attributes = ['window_id'] def consume(self, line, *args): - window_id, = line + window_id, = line.split(b' ') self.window_id = window_id @@ -149,7 +151,7 @@ class UnlinkedWindowClose(Notification): attributes = ['window_id'] def consume(self, line, *args): - window_id, = line + window_id, = line.split(b' ') self.window_id = window_id @@ -160,7 +162,7 @@ class WindowClose(Notification): attributes = ['window_id'] def consume(self, line, *args): - window_id, = line + window_id, = line.split(b' ') self.window_id = window_id @@ -171,7 +173,7 @@ class UnlinkedWindowRenamed(Notification): attributes = ['window_id', 'window_name'] def consume(self, line, *args): - window_id, window_name = line + window_id, window_name = line.split(b' ') self.window_id = window_id self.window_name = window_name @@ -183,7 +185,7 @@ class WindowRenamed(Notification): attributes = ['window_id', 'window_name'] def consume(self, line, *args): - window_id, window_name = line + window_id, window_name = line.split(b' ') self.window_id = window_id self.window_name = window_name @@ -227,7 +229,9 @@ def handle_output(self, notification): assert isinstance(notification, Output) pane_id = notification.pane_id output = notification.output - terminal = self.terminator.pane_id_to_terminal.get(pane_id) + dbg(pane_id) + dbg(self.terminator.pane_id_to_terminal) + terminal = self.terminator.pane_id_to_terminal.get(pane_id.decode()) if not terminal: return for code in ALTERNATE_SCREEN_ENTER_CODES: @@ -239,7 +243,9 @@ def handle_output(self, notification): # NOTE: using neovim, enabling visual-bell and setting t_vb empty results in incorrect # escape sequences (C-g) being printed in the neovim window; remove them until we can # figure out the root cause - terminal.vte.feed(output.decode('string_escape').replace("\033g","")) + # terminal.vte.feed(output.replace("\033g", "").encode('utf-8')) + dbg(output) + terminal.vte.feed(output.decode('unicode-escape').encode('latin-1')) def handle_layout_change(self, notification): assert isinstance(notification, LayoutChange) @@ -267,7 +273,7 @@ def garbage_collect_panes_result(self, result): removed_pane_ids = pane_id_to_terminal.keys() for line in result: - pane_id, pane_pid = line.split(' ') + pane_id, pane_pid = line.split(b' ') try: removed_pane_ids.remove(pane_id) pane_id_to_terminal[pane_id].pid = pane_pid @@ -288,7 +294,14 @@ def initial_layout_result(self, result): window_layouts = [] for line in result: window_layout = line.strip() - window_layouts.extend(layout.parse_layout(self.layout_parser.parse(window_layout)[0])) + dbg(window_layout) + try: + parsed_layout = self.layout_parser.parse(window_layout.decode()) + except Exception as e: + dbg(e) + exit(1) + dbg(parsed_layout) + window_layouts.extend(layout.parse_layout(parsed_layout[0])) # window_layouts.append(layout.parse_layout(window_layout)) terminator_layout = layout.convert_to_terminator_layout( window_layouts) From 68d2a8c8b5972edf294d9ba6b60b4aae84ef8fac Mon Sep 17 00:00:00 2001 From: Jack Gaino Date: Sat, 20 Mar 2021 22:22:19 -0400 Subject: [PATCH 09/29] Add a bunch of embarrassing debug messages --- terminatorlib/tmux/control.py | 11 ++++++++++- terminatorlib/tmux/notifications.py | 10 +++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/terminatorlib/tmux/control.py b/terminatorlib/tmux/control.py index 66f7588e5..69a23cced 100644 --- a/terminatorlib/tmux/control.py +++ b/terminatorlib/tmux/control.py @@ -173,6 +173,7 @@ def garbage_collect_panes(self): callback=('garbage_collect_panes_result',)) def initial_layout(self): + dbg('running initial layout') # JACK_TEST self._run_command( 'list-windows -t {} -F "#{{window_layout}}"' .format(self.session_name), @@ -243,6 +244,7 @@ def send_quoted_content(self, content, pane_id): pane_id, key_name_lookup, content)) def _run_command(self, command, callback=None): + dbg('running command {}'.format(command)) # JACK_TEST if not self.input: dbg('No tmux connection. [command={}]'.format(command)) else: @@ -252,6 +254,7 @@ def _run_command(self, command, callback=None): dbg("Tmux server has gone away.") return callback = callback or notifications.noop + dbg('adding callback: {}'.format(callback)) # JACK_TEST self.requests.put(callback) @staticmethod @@ -269,6 +272,7 @@ def consume_notifications(self): while True: try: if self.tmux.poll() is not None: + dbg('uhhh') # JACK_TEST break except AttributeError as e: dbg("Tmux control instance was reset.") @@ -287,7 +291,12 @@ def consume_notifications(self): dbg("Discarding invalid output from the control terminal.") continue notification.consume(line, self.output) - handler.handle(notification) + dbg('consumed notification') # JACK_TEST + try: # JACK_TEST + handler.handle(notification) + except Exception as e: # JACK_TEST + dbg("UH OH ------------------------------------------------- {}".format(e)) # JACK_TEST + dbg('handled notification') # JACK_TEST handler.terminate() def display_pane_tty(self, pane_id): diff --git a/terminatorlib/tmux/notifications.py b/terminatorlib/tmux/notifications.py index 6d205c6bf..14a608f53 100644 --- a/terminatorlib/tmux/notifications.py +++ b/terminatorlib/tmux/notifications.py @@ -198,16 +198,22 @@ def __init__(self, terminator): def handle(self, notification): try: + dbg('looking for method for {}'.format(notification.marker)) # JACK_TEST handler_method = getattr(self, 'handle_{}'.format( notification.marker.replace('-', '_'))) handler_method(notification) - except AttributeError: + except AttributeError as e: # JACK_TEST + dbg('------- method for {} NOT FOUND: {}'.format(notification.marker, e)) # JACK_TEST pass + except Exception as e: # JACK_TEST + dbg('something went wrong while handling {}: {}'.format(notification.marker, e)) # JACK_TEST def handle_begin(self, notification): dbg('### {}'.format(notification)) assert isinstance(notification, Result) + dbg('######## getting callback') # JACK_TEST callback = self.terminator.tmux_control.requests.get() + dbg(callback) # JACK_TEST if notification.error: dbg('Request error: {}'.format(notification)) if notification.result[0] in ATTACH_ERROR_STRINGS: @@ -291,6 +297,7 @@ def callback(): GObject.idle_add(callback) def initial_layout_result(self, result): + dbg('checking window layout') # JACK_TEST window_layouts = [] for line in result: window_layout = line.strip() @@ -303,6 +310,7 @@ def initial_layout_result(self, result): dbg(parsed_layout) window_layouts.extend(layout.parse_layout(parsed_layout[0])) # window_layouts.append(layout.parse_layout(window_layout)) + dbg('window layouts: {}'.format(window_layouts)) # JACK_TEST terminator_layout = layout.convert_to_terminator_layout( window_layouts) import pprint From 17810e67b26aa4f41608295f41e9795fcb109fdd Mon Sep 17 00:00:00 2001 From: Jack Gaino Date: Sat, 20 Mar 2021 22:25:09 -0400 Subject: [PATCH 10/29] Reformat with Black --- terminatorlib/tmux/control.py | 215 ++++++++++++++++------------ terminatorlib/tmux/layout.py | 174 ++++++++++------------ terminatorlib/tmux/notifications.py | 163 +++++++++++---------- 3 files changed, 285 insertions(+), 267 deletions(-) diff --git a/terminatorlib/tmux/control.py b/terminatorlib/tmux/control.py index 69a23cced..362f98bfa 100644 --- a/terminatorlib/tmux/control.py +++ b/terminatorlib/tmux/control.py @@ -9,42 +9,38 @@ from terminatorlib.tmux import notifications from terminatorlib.util import dbg -ESCAPE_CODE = '\033' -TMUX_BINARY = 'tmux' +ESCAPE_CODE = "\033" +TMUX_BINARY = "tmux" + def esc(seq): - return '{}{}'.format(ESCAPE_CODE, seq) + return "{}{}".format(ESCAPE_CODE, seq) KEY_MAPPINGS = { - Gdk.KEY_BackSpace: '\b', - Gdk.KEY_Tab: '\t', - Gdk.KEY_Insert: esc('[2~'), - Gdk.KEY_Delete: esc('[3~'), - Gdk.KEY_Page_Up: esc('[5~'), - Gdk.KEY_Page_Down: esc('[6~'), - Gdk.KEY_Home: esc('[1~'), - Gdk.KEY_End: esc('[4~'), - Gdk.KEY_Up: esc('[A'), - Gdk.KEY_Down: esc('[B'), - Gdk.KEY_Right: esc('[C'), - Gdk.KEY_Left: esc('[D'), -} -ARROW_KEYS = { - Gdk.KEY_Up, - Gdk.KEY_Down, - Gdk.KEY_Left, - Gdk.KEY_Right + Gdk.KEY_BackSpace: "\b", + Gdk.KEY_Tab: "\t", + Gdk.KEY_Insert: esc("[2~"), + Gdk.KEY_Delete: esc("[3~"), + Gdk.KEY_Page_Up: esc("[5~"), + Gdk.KEY_Page_Down: esc("[6~"), + Gdk.KEY_Home: esc("[1~"), + Gdk.KEY_End: esc("[4~"), + Gdk.KEY_Up: esc("[A"), + Gdk.KEY_Down: esc("[B"), + Gdk.KEY_Right: esc("[C"), + Gdk.KEY_Left: esc("[D"), } +ARROW_KEYS = {Gdk.KEY_Up, Gdk.KEY_Down, Gdk.KEY_Left, Gdk.KEY_Right} MOUSE_WHEEL = { # TODO: make it configurable, e.g. like better-mouse-mode plugin Gdk.ScrollDirection.UP: "C-y C-y C-y", Gdk.ScrollDirection.DOWN: "C-e C-e C-e", } + # TODO: implement ssh connection using paramiko class TmuxControl(object): - def __init__(self, session_name, notifications_handler): self.session_name = session_name self.notifications_handler = notifications_handler @@ -67,10 +63,10 @@ def remote_connect(self, command): dbg("Already connected.") return popen_command = "ssh " + self.remote - self.tmux = subprocess.Popen(popen_command, - stdout=subprocess.PIPE, - stdin=subprocess.PIPE, shell=True) - self.input = self.tmux.stdin + self.tmux = subprocess.Popen( + popen_command, stdout=subprocess.PIPE, stdin=subprocess.PIPE, shell=True + ) + self.input = self.tmux.stdin self.output = self.tmux.stdout self.run_remote_command(command) @@ -79,77 +75,93 @@ def run_remote_command(self, popen_command): popen_command = map(quote, popen_command) command = " ".join(popen_command) if not self.input: - dbg('No tmux connection. [command={}]'.format(command)) + dbg("No tmux connection. [command={}]".format(command)) else: try: - self.input.write('exec {}\n'.format(command).encode()) + self.input.write("exec {}\n".format(command).encode()) except IOError: dbg("Tmux server has gone away.") return - def run_command(self, command, marker, cwd=None, orientation=None, - pane_id=None): + def run_command(self, command, marker, cwd=None, orientation=None, pane_id=None): if self.input: if orientation: - self.split_window(cwd=cwd, orientation=orientation, - pane_id=pane_id, command=command, - marker=marker) + self.split_window( + cwd=cwd, + orientation=orientation, + pane_id=pane_id, + command=command, + marker=marker, + ) else: self.new_window(cwd=cwd, command=command, marker=marker) else: self.new_session(cwd=cwd, command=command, marker=marker) - def split_window(self, cwd, orientation, pane_id, - command=None, marker=''): - orientation = '-h' if orientation == 'horizontal' else '-v' + def split_window(self, cwd, orientation, pane_id, command=None, marker=""): + orientation = "-h" if orientation == "horizontal" else "-v" tmux_command = 'split-window {} -t {} -P -F "#D {}"'.format( - orientation, pane_id, marker) + orientation, pane_id, marker + ) if cwd: tmux_command += ' -c "{}"'.format(cwd) if command: tmux_command += ' "{}"'.format(command) - self._run_command(tmux_command, - callback=('pane_id_result',)) + self._run_command(tmux_command, callback=("pane_id_result",)) - def new_window(self, cwd=None, command=None, marker=''): + def new_window(self, cwd=None, command=None, marker=""): tmux_command = 'new-window -P -F "#D {}"'.format(marker) if cwd: tmux_command += ' -c "{}"'.format(cwd) if command: tmux_command += ' "{}"'.format(command) - self._run_command(tmux_command, - callback=('pane_id_result',)) + self._run_command(tmux_command, callback=("pane_id_result",)) def attach_session(self): - popen_command = [TMUX_BINARY, '-2', '-C', 'attach-session', - '-t', self.session_name] + popen_command = [ + TMUX_BINARY, + "-2", + "-C", + "attach-session", + "-t", + self.session_name, + ] if self.remote: self.remote_connect(popen_command) if not self.tmux: - self.tmux = subprocess.Popen(popen_command, - stdout=subprocess.PIPE, - stdin=subprocess.PIPE, bufsize=0) + self.tmux = subprocess.Popen( + popen_command, stdout=subprocess.PIPE, stdin=subprocess.PIPE, bufsize=0 + ) self.input = self.tmux.stdin self.output = self.tmux.stdout self.requests.put(notifications.noop) self.start_notifications_consumer() self.initial_layout() - def new_session(self, cwd=None, command=None, marker=''): - popen_command = [TMUX_BINARY, '-2', '-C', 'new-session', '-s', self.session_name, - '-P', '-F', '#D {}'.format(marker)] + def new_session(self, cwd=None, command=None, marker=""): + popen_command = [ + TMUX_BINARY, + "-2", + "-C", + "new-session", + "-s", + self.session_name, + "-P", + "-F", + "#D {}".format(marker), + ] if cwd and not self.remote: - popen_command += ['-c', cwd] + popen_command += ["-c", cwd] if command: popen_command.append(command) if self.remote: self.remote_connect(popen_command) if not self.tmux: - self.tmux = subprocess.Popen(popen_command, - stdout=subprocess.PIPE, - stdin=subprocess.PIPE) + self.tmux = subprocess.Popen( + popen_command, stdout=subprocess.PIPE, stdin=subprocess.PIPE + ) self.input = self.tmux.stdin self.output = self.tmux.stdout # starting a new session, delete any old requests we may have @@ -158,37 +170,42 @@ def new_session(self, cwd=None, command=None, marker=''): while not self.requests.empty(): self.requests.get(timeout=1) - self.requests.put(('pane_id_result',)) + self.requests.put(("pane_id_result",)) self.start_notifications_consumer() def refresh_client(self, width, height): - dbg('{}::{}: {}x{}'.format("TmuxControl", "refresh_client", width, height)) + dbg("{}::{}: {}x{}".format("TmuxControl", "refresh_client", width, height)) self.width = width self.height = height - self._run_command('refresh-client -C {},{}'.format(width, height)) + self._run_command("refresh-client -C {},{}".format(width, height)) def garbage_collect_panes(self): - self._run_command('list-panes -s -t {} -F "#D {}"'.format( - self.session_name, '#{pane_pid}'), - callback=('garbage_collect_panes_result',)) + self._run_command( + 'list-panes -s -t {} -F "#D {}"'.format(self.session_name, "#{pane_pid}"), + callback=("garbage_collect_panes_result",), + ) def initial_layout(self): - dbg('running initial layout') # JACK_TEST + dbg("running initial layout") # JACK_TEST self._run_command( - 'list-windows -t {} -F "#{{window_layout}}"' - .format(self.session_name), - callback=('initial_layout_result',)) + 'list-windows -t {} -F "#{{window_layout}}"'.format(self.session_name), + callback=("initial_layout_result",), + ) def initial_output(self, pane_id): self._run_command( - 'capture-pane -J -p -t {} -eC -S - -E -'.format(pane_id), - callback=('result_callback', pane_id) + "capture-pane -J -p -t {} -eC -S - -E -".format(pane_id), + callback=("result_callback", pane_id), ) def toggle_zoom(self, pane_id, zoom=False): self.is_zoomed = not self.is_zoomed if not zoom: - self._run_command('resize-pane -Z -x {} -y {} -t {}'.format(self.width, self.height, pane_id)) + self._run_command( + "resize-pane -Z -x {} -y {} -t {}".format( + self.width, self.height, pane_id + ) + ) def send_keypress(self, event, pane_id): keyval = event.keyval @@ -197,29 +214,32 @@ def send_keypress(self, event, pane_id): if keyval in KEY_MAPPINGS: key = KEY_MAPPINGS[keyval] if keyval in ARROW_KEYS and state & Gdk.ModifierType.CONTROL_MASK: - key = '{}1;5{}'.format(key[:2], key[2:]) + key = "{}1;5{}".format(key[:2], key[2:]) else: key = event.string if state & Gdk.ModifierType.MOD1_MASK: # Hack to have CTRL+SHIFT+Alt PageUp/PageDown/Home/End # work without these silly [... escaped characters - if state & (Gdk.ModifierType.CONTROL_MASK | - Gdk.ModifierType.SHIFT_MASK): + if state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK): return else: key = esc(key) - if key == ';': - key = '\\;' + if key == ";": + key = "\\;" self.send_content(key, pane_id) # Handle mouse scrolling events if the alternate_screen is visible # otherwise let Terminator handle all the mouse behavior def send_mousewheel(self, event, pane_id): - SMOOTH_SCROLL_UP = event.direction == Gdk.ScrollDirection.SMOOTH and event.delta_y <= 0. - SMOOTH_SCROLL_DOWN = event.direction == Gdk.ScrollDirection.SMOOTH and event.delta_y > 0. + SMOOTH_SCROLL_UP = ( + event.direction == Gdk.ScrollDirection.SMOOTH and event.delta_y <= 0.0 + ) + SMOOTH_SCROLL_DOWN = ( + event.direction == Gdk.ScrollDirection.SMOOTH and event.delta_y > 0.0 + ) if SMOOTH_SCROLL_UP: wheel = MOUSE_WHEEL[Gdk.ScrollDirection.UP] elif SMOOTH_SCROLL_DOWN: @@ -235,31 +255,35 @@ def send_mousewheel(self, event, pane_id): def send_content(self, content, pane_id): key_name_lookup = "-l" if ESCAPE_CODE in content else "" quote = "'" if "'" not in content else '"' - self._run_command("send-keys -t {} {} -- {}{}{}".format( - pane_id, key_name_lookup, quote, content, quote)) + self._run_command( + "send-keys -t {} {} -- {}{}{}".format( + pane_id, key_name_lookup, quote, content, quote + ) + ) def send_quoted_content(self, content, pane_id): key_name_lookup = "-l" if ESCAPE_CODE in content else "" - self._run_command("send-keys -t {} {} -- {}".format( - pane_id, key_name_lookup, content)) + self._run_command( + "send-keys -t {} {} -- {}".format(pane_id, key_name_lookup, content) + ) def _run_command(self, command, callback=None): - dbg('running command {}'.format(command)) # JACK_TEST + dbg("running command {}".format(command)) # JACK_TEST if not self.input: - dbg('No tmux connection. [command={}]'.format(command)) + dbg("No tmux connection. [command={}]".format(command)) else: try: - self.input.write('{}\n'.format(command).encode()) + self.input.write("{}\n".format(command).encode()) except IOError: dbg("Tmux server has gone away.") return callback = callback or notifications.noop - dbg('adding callback: {}'.format(callback)) # JACK_TEST + dbg("adding callback: {}".format(callback)) # JACK_TEST self.requests.put(callback) @staticmethod def kill_server(): - command = [TMUX_BINARY, 'kill-session', '-t', 'terminator'] + command = [TMUX_BINARY, "kill-session", "-t", "terminator"] subprocess.call(command) def start_notifications_consumer(self): @@ -272,7 +296,7 @@ def consume_notifications(self): while True: try: if self.tmux.poll() is not None: - dbg('uhhh') # JACK_TEST + dbg("uhhh") # JACK_TEST break except AttributeError as e: dbg("Tmux control instance was reset.") @@ -280,8 +304,8 @@ def consume_notifications(self): line = self.output.readline()[:-1] if not line: continue - dbg('=>>>>> LINE RECEIVED: {}'.format(line)) - line = line[1:].split(b' ', 1) + dbg("=>>>>> LINE RECEIVED: {}".format(line)) + line = line[1:].split(b" ", 1) marker = line[0].decode() line = line[1] # skip MOTD, anything that isn't coming from tmux control mode @@ -291,27 +315,28 @@ def consume_notifications(self): dbg("Discarding invalid output from the control terminal.") continue notification.consume(line, self.output) - dbg('consumed notification') # JACK_TEST + dbg("consumed notification") # JACK_TEST try: # JACK_TEST handler.handle(notification) except Exception as e: # JACK_TEST - dbg("UH OH ------------------------------------------------- {}".format(e)) # JACK_TEST - dbg('handled notification') # JACK_TEST + dbg( + "UH OH ------------------------------------------------- {}".format( + e + ) + ) # JACK_TEST + dbg("handled notification") # JACK_TEST handler.terminate() def display_pane_tty(self, pane_id): - tmux_command = 'display -pt "{}" "#D {}"'.format( - pane_id, "#{pane_tty}") + tmux_command = 'display -pt "{}" "#D {}"'.format(pane_id, "#{pane_tty}") - self._run_command(tmux_command, - callback=('pane_tty_result',)) + self._run_command(tmux_command, callback=("pane_tty_result",)) def resize_pane(self, pane_id, rows, cols): if self.is_zoomed: # if the pane is zoomed, there is no need for tmux to # change the current layout return - tmux_command = 'resize-pane -t "{}" -x {} -y {}'.format( - pane_id, cols, rows) + tmux_command = 'resize-pane -t "{}" -x {} -y {}'.format(pane_id, cols, rows) self._run_command(tmux_command) diff --git a/terminatorlib/tmux/layout.py b/terminatorlib/tmux/layout.py index 56edb00ce..6dcd6c596 100644 --- a/terminatorlib/tmux/layout.py +++ b/terminatorlib/tmux/layout.py @@ -1,6 +1,7 @@ from pyparsing import * -class LayoutParser(): + +class LayoutParser: """BNF representation for a Tmux Layout :: + ; :: ( | ) ? ; @@ -17,22 +18,23 @@ class LayoutParser(): :: | "a" | ... | "f" ; :: "," ; """ + layout_parser = None def __init__(self): decimal = Word(nums) - comma = Suppress(Literal(',')) - start_token = Literal('{') | Literal('[') - end_token = Suppress(Literal('}') | Literal(']')) + comma = Suppress(Literal(",")) + start_token = Literal("{") | Literal("[") + end_token = Suppress(Literal("}") | Literal("]")) layout_name = Suppress(Word(hexnums, min=4, max=4)) - size = decimal("width") + Suppress(Literal('x')) + decimal("height") + size = decimal("width") + Suppress(Literal("x")) + decimal("height") - preamble = size + comma + decimal("x") + comma + decimal("y") - pane = Group(preamble + comma + decimal("pane_id")) - element = Forward() # will be defined later - container = Group(preamble + start_token + OneOrMore(element) + end_token) + preamble = size + comma + decimal("x") + comma + decimal("y") + pane = Group(preamble + comma + decimal("pane_id")) + element = Forward() # will be defined later + container = Group(preamble + start_token + OneOrMore(element) + end_token) element << (container | pane) + Optional(comma) @@ -42,6 +44,7 @@ def parse(self, layout): parsed = self.layout_parser.parseString(layout) return parsed.asList() + def parse_layout(layout): """Apply our application logic to the parsed layout. @@ -65,61 +68,40 @@ def parse_layout(layout): for item in layout[5:]: children.extend(parse_layout(item)) - if layout[4] == '{': - result.append(Horizontal( - layout[0], - layout[1], - layout[2], - layout[3], - children - )) - - elif layout[4] == '[': - result.append(Vertical( - layout[0], - layout[1], - layout[2], - layout[3], - children - )) + if layout[4] == "{": + result.append(Horizontal(layout[0], layout[1], layout[2], layout[3], children)) + + elif layout[4] == "[": + result.append(Vertical(layout[0], layout[1], layout[2], layout[3], children)) else: - result.append(Pane( - layout[0], - layout[1], - layout[2], - layout[3], - "%{}".format(layout[4]) - )) + result.append( + Pane(layout[0], layout[1], layout[2], layout[3], "%{}".format(layout[4])) + ) return result + def convert_to_terminator_layout(window_layouts): assert len(window_layouts) > 0 result = {} pane_index = 0 - window_name = 'window0' + window_name = "window0" parent_name = window_name - result[window_name] = { - 'type': 'Window', - 'parent': '' - } + result[window_name] = {"type": "Window", "parent": ""} if len(window_layouts) > 1: - notebook_name = 'notebook0' - result[notebook_name] = { - 'type': 'Notebook', - 'parent': parent_name - } + notebook_name = "notebook0" + result[notebook_name] = {"type": "Notebook", "parent": parent_name} parent_name = notebook_name order = 0 for window_layout in window_layouts: converter = _get_converter(window_layout) pane_index, order = converter( - result, parent_name, window_layout, pane_index, order) + result, parent_name, window_layout, pane_index, order + ) return result class Container(object): - def __init__(self, width, height, x, y): self.width = width self.height = height @@ -127,11 +109,14 @@ def __init__(self, width, height, x, y): self.y = y def __str__(self): - return ( - '{}[width={}, height={}, x={}, y={}, {}]' - .format(self.__class__.__name__, - self.width, self.height, self.x, self.y, - self._child_str())) + return "{}[width={}, height={}, x={}, y={}, {}]".format( + self.__class__.__name__, + self.width, + self.height, + self.x, + self.y, + self._child_str(), + ) __repr__ = __str__ @@ -140,67 +125,64 @@ def _child_str(self): class Pane(Container): - def __init__(self, width, height, x, y, pane_id): super(Pane, self).__init__(width, height, x, y) self.pane_id = pane_id def _child_str(self): - return 'pane_id={}'.format(self.pane_id) + return "pane_id={}".format(self.pane_id) class Vertical(Container): - def __init__(self, width, height, x, y, children): super(Vertical, self).__init__(width, height, x, y) self.children = children def _child_str(self): - return 'children={}'.format(self.children) + return "children={}".format(self.children) class Horizontal(Container): - def __init__(self, width, height, x, y, children): super(Horizontal, self).__init__(width, height, x, y) self.children = children def _child_str(self): - return 'children={}'.format(self.children) + return "children={}".format(self.children) def _covert_pane_to_terminal(result, parent_name, pane, pane_index, order): assert isinstance(pane, Pane) - terminal = _convert(parent_name, 'Terminal', pane, order) + terminal = _convert(parent_name, "Terminal", pane, order) order += 1 - terminal['tmux']['pane_id'] = pane.pane_id - result['terminal{}'.format(pane.pane_id[1:])] = terminal + terminal["tmux"]["pane_id"] = pane.pane_id + result["terminal{}".format(pane.pane_id[1:])] = terminal return pane_index, order -def _convert_vertical_to_vpane(result, parent_name, vertical_or_children, - pane_index, order): +def _convert_vertical_to_vpane( + result, parent_name, vertical_or_children, pane_index, order +): return _convert_container_to_terminator_pane( - result, parent_name, vertical_or_children, pane_index, Vertical, - order) + result, parent_name, vertical_or_children, pane_index, Vertical, order + ) -def _convert_horizontal_to_hpane(result, parent_name, horizontal_or_children, - pane_index, order): +def _convert_horizontal_to_hpane( + result, parent_name, horizontal_or_children, pane_index, order +): return _convert_container_to_terminator_pane( - result, parent_name, horizontal_or_children, pane_index, - Horizontal, order) + result, parent_name, horizontal_or_children, pane_index, Horizontal, order + ) -def _convert_container_to_terminator_pane(result, parent_name, - container_or_children, - pane_index, pane_type, - order): - terminator_type = 'VPaned' if issubclass(pane_type, Vertical) else 'HPaned' +def _convert_container_to_terminator_pane( + result, parent_name, container_or_children, pane_index, pane_type, order +): + terminator_type = "VPaned" if issubclass(pane_type, Vertical) else "HPaned" if isinstance(container_or_children, pane_type): container = container_or_children - pane = _convert(parent_name, terminator_type, container_or_children, - order) + pane = _convert(parent_name, terminator_type, container_or_children, order) order += 1 children = container.children else: @@ -208,33 +190,25 @@ def _convert_container_to_terminator_pane(result, parent_name, if len(children) == 1: child = children[0] child_converter = _get_converter(child) - return child_converter(result, parent_name, child, pane_index, - order) - pane = { - 'type': terminator_type, - 'parent': parent_name - } - pane_name = 'pane{}'.format(pane_index) + return child_converter(result, parent_name, child, pane_index, order) + pane = {"type": terminator_type, "parent": parent_name} + pane_name = "pane{}".format(pane_index) result[pane_name] = pane parent_name = pane_name pane_index += 1 child1 = children[0] child1_converter = _get_converter(child1) - pane_index, order = child1_converter(result, parent_name, child1, - pane_index, order) - pane_index, order = _convert_container_to_terminator_pane(result, - parent_name, - children[1:], - pane_index, - pane_type, - order) + pane_index, order = child1_converter(result, parent_name, child1, pane_index, order) + pane_index, order = _convert_container_to_terminator_pane( + result, parent_name, children[1:], pane_index, pane_type, order + ) return pane_index, order converters = { Pane: _covert_pane_to_terminal, Vertical: _convert_vertical_to_vpane, - Horizontal: _convert_horizontal_to_hpane + Horizontal: _convert_horizontal_to_hpane, } @@ -242,19 +216,19 @@ def _get_converter(container): try: return converters[type(container)] except KeyError: - raise ValueError('Illegal window layout: {}'.format(container)) + raise ValueError("Illegal window layout: {}".format(container)) def _convert(parent_name, type_name, container, order): assert isinstance(container, Container) return { - 'type': type_name, - 'parent': parent_name, - 'order': order, - 'tmux': { - 'width': container.width, - 'height': container.height, - 'x': container.x, - 'y': container.y - } + "type": type_name, + "parent": parent_name, + "order": order, + "tmux": { + "width": container.width, + "height": container.height, + "x": container.x, + "y": container.y, + }, } diff --git a/terminatorlib/tmux/notifications.py b/terminatorlib/tmux/notifications.py index 14a608f53..075022fef 100644 --- a/terminatorlib/tmux/notifications.py +++ b/terminatorlib/tmux/notifications.py @@ -4,9 +4,14 @@ from terminatorlib.tmux import layout import string -ATTACH_ERROR_STRINGS = [b"can't find session terminator", b"no current session", b"no sessions"] -ALTERNATE_SCREEN_ENTER_CODES = [ b"\\033[?1049h" ] -ALTERNATE_SCREEN_EXIT_CODES = [ b"\\033[?1049l" ] + +ATTACH_ERROR_STRINGS = [ + b"can't find session terminator", + b"no current session", + b"no sessions", +] +ALTERNATE_SCREEN_ENTER_CODES = [b"\\033[?1049h"] +ALTERNATE_SCREEN_EXIT_CODES = [b"\\033[?1049l"] notifications_mappings = {} @@ -18,45 +23,46 @@ def notification(cls): class Notification(object): - marker = 'undefined' + marker = "undefined" attributes = [] def consume(self, line, out): pass def __str__(self): - attributes = ['{}="{}"'.format(attribute, getattr(self, attribute, '')) - for attribute in self.attributes] - return '{}[{}]'.format(self.marker, ', '.join(attributes)) + attributes = [ + '{}="{}"'.format(attribute, getattr(self, attribute, "")) + for attribute in self.attributes + ] + return "{}[{}]".format(self.marker, ", ".join(attributes)) @notification class Result(Notification): - marker = 'begin' - attributes = ['begin_timestamp', 'code', 'result', 'end_timestamp', - 'error'] + marker = "begin" + attributes = ["begin_timestamp", "code", "result", "end_timestamp", "error"] def consume(self, line, out): - timestamp, code, _ = line.split(b' ') + timestamp, code, _ = line.split(b" ") self.begin_timestamp = timestamp self.code = code result = [] line = out.readline()[:-1] - while not (line.startswith(b'%end') or line.startswith(b'%error')): + while not (line.startswith(b"%end") or line.startswith(b"%error")): result.append(line) line = out.readline()[:-1] self.result = result - end, timestamp, code, _ = line.split(b' ') + end, timestamp, code, _ = line.split(b" ") self.end_timestamp = timestamp - self.error = end == b'%error' + self.error = end == b"%error" @notification class Exit(Notification): - marker = 'exit' - attributes = ['reason'] + marker = "exit" + attributes = ["reason"] def consume(self, line, *args): self.reason = line[0] if line else None @@ -65,40 +71,43 @@ def consume(self, line, *args): @notification class LayoutChange(Notification): - marker = 'layout-change' - attributes = ['window_id', 'window_layout', 'window_visible_layout', - 'window_flags'] + marker = "layout-change" + attributes = ["window_id", "window_layout", "window_visible_layout", "window_flags"] def consume(self, line, *args): # attributes not present default to None - line_items = line.split(b' ') - window_id, window_layout, window_visible_layout, window_flags = line_items + [None] * (len(self.attributes) - len(line_items)) + line_items = line.split(b" ") + window_id, window_layout, window_visible_layout, window_flags = line_items + [ + None + ] * (len(self.attributes) - len(line_items)) self.window_id = window_id self.window_layout = window_layout self.window_visible_layout = window_visible_layout self.window_flags = window_flags + @notification class Output(Notification): - marker = 'output' - attributes = ['pane_id', 'output'] + marker = "output" + attributes = ["pane_id", "output"] def consume(self, line, *args): # pane_id = line[0] # output = ' '.join(line[1:]) - pane_id, output = line.split(b' ', 1) + pane_id, output = line.split(b" ", 1) self.pane_id = pane_id self.output = output + @notification class SessionChanged(Notification): - marker = 'session-changed' - attributes = ['session_id', 'session_name'] + marker = "session-changed" + attributes = ["session_id", "session_name"] def consume(self, line, *args): - session_id, session_name = line.split(b' ') + session_id, session_name = line.split(b" ") self.session_id = session_id self.session_name = session_name @@ -106,11 +115,11 @@ def consume(self, line, *args): @notification class SessionRenamed(Notification): - marker = 'session-renamed' - attributes = ['session_id', 'session_name'] + marker = "session-renamed" + attributes = ["session_id", "session_name"] def consume(self, line, *args): - session_id, session_name = line.split(b' ') + session_id, session_name = line.split(b" ") self.session_id = session_id self.session_name = session_name @@ -118,62 +127,62 @@ def consume(self, line, *args): @notification class SessionsChanged(Notification): - marker = 'sessions-changed' + marker = "sessions-changed" attributes = [] @notification class UnlinkedWindowAdd(Notification): - marker = 'unlinked-window-add' - attributes = ['window_id'] + marker = "unlinked-window-add" + attributes = ["window_id"] def consume(self, line, *args): - window_id, = line.split(b' ') + (window_id,) = line.split(b" ") self.window_id = window_id @notification class WindowAdd(Notification): - marker = 'window-add' - attributes = ['window_id'] + marker = "window-add" + attributes = ["window_id"] def consume(self, line, *args): - window_id, = line.split(b' ') + (window_id,) = line.split(b" ") self.window_id = window_id @notification class UnlinkedWindowClose(Notification): - marker = 'unlinked-window-close' - attributes = ['window_id'] + marker = "unlinked-window-close" + attributes = ["window_id"] def consume(self, line, *args): - window_id, = line.split(b' ') + (window_id,) = line.split(b" ") self.window_id = window_id @notification class WindowClose(Notification): - marker = 'window-close' - attributes = ['window_id'] + marker = "window-close" + attributes = ["window_id"] def consume(self, line, *args): - window_id, = line.split(b' ') + (window_id,) = line.split(b" ") self.window_id = window_id @notification class UnlinkedWindowRenamed(Notification): - marker = 'unlinked-window-renamed' - attributes = ['window_id', 'window_name'] + marker = "unlinked-window-renamed" + attributes = ["window_id", "window_name"] def consume(self, line, *args): - window_id, window_name = line.split(b' ') + window_id, window_name = line.split(b" ") self.window_id = window_id self.window_name = window_name @@ -181,41 +190,47 @@ def consume(self, line, *args): @notification class WindowRenamed(Notification): - marker = 'window-renamed' - attributes = ['window_id', 'window_name'] + marker = "window-renamed" + attributes = ["window_id", "window_name"] def consume(self, line, *args): - window_id, window_name = line.split(b' ') + window_id, window_name = line.split(b" ") self.window_id = window_id self.window_name = window_name class NotificationsHandler(object): - def __init__(self, terminator): self.terminator = terminator self.layout_parser = layout.LayoutParser() def handle(self, notification): try: - dbg('looking for method for {}'.format(notification.marker)) # JACK_TEST - handler_method = getattr(self, 'handle_{}'.format( - notification.marker.replace('-', '_'))) + dbg("looking for method for {}".format(notification.marker)) # JACK_TEST + handler_method = getattr( + self, "handle_{}".format(notification.marker.replace("-", "_")) + ) handler_method(notification) except AttributeError as e: # JACK_TEST - dbg('------- method for {} NOT FOUND: {}'.format(notification.marker, e)) # JACK_TEST + dbg( + "------- method for {} NOT FOUND: {}".format(notification.marker, e) + ) # JACK_TEST pass except Exception as e: # JACK_TEST - dbg('something went wrong while handling {}: {}'.format(notification.marker, e)) # JACK_TEST + dbg( + "something went wrong while handling {}: {}".format( + notification.marker, e + ) + ) # JACK_TEST def handle_begin(self, notification): - dbg('### {}'.format(notification)) + dbg("### {}".format(notification)) assert isinstance(notification, Result) - dbg('######## getting callback') # JACK_TEST + dbg("######## getting callback") # JACK_TEST callback = self.terminator.tmux_control.requests.get() dbg(callback) # JACK_TEST if notification.error: - dbg('Request error: {}'.format(notification)) + dbg("Request error: {}".format(notification)) if notification.result[0] in ATTACH_ERROR_STRINGS: # if we got here it means that attaching to an existing session # failed, invalidate the layout so the Terminator initialization @@ -251,7 +266,7 @@ def handle_output(self, notification): # figure out the root cause # terminal.vte.feed(output.replace("\033g", "").encode('utf-8')) dbg(output) - terminal.vte.feed(output.decode('unicode-escape').encode('latin-1')) + terminal.vte.feed(output.decode("unicode-escape").encode("latin-1")) def handle_layout_change(self, notification): assert isinstance(notification, LayoutChange) @@ -262,7 +277,7 @@ def handle_window_close(self, notification): GObject.idle_add(self.terminator.tmux_control.garbage_collect_panes) def pane_id_result(self, result): - pane_id, marker = result[0].split(' ') + pane_id, marker = result[0].split(" ") terminal = self.terminator.find_terminal_by_pane_id(marker) terminal.pane_id = pane_id self.terminator.pane_id_to_terminal[pane_id] = terminal @@ -271,7 +286,7 @@ def pane_id_result(self, result): # the Terminal class first def pane_tty_result(self, result): dbg(result) - pane_id, pane_tty = result[0].split(' ') + pane_id, pane_tty = result[0].split(" ") # self.terminator.pane_id_to_terminal[pane_id].tty = pane_tty def garbage_collect_panes_result(self, result): @@ -279,7 +294,7 @@ def garbage_collect_panes_result(self, result): removed_pane_ids = pane_id_to_terminal.keys() for line in result: - pane_id, pane_pid = line.split(b' ') + pane_id, pane_pid = line.split(b" ") try: removed_pane_ids.remove(pane_id) pane_id_to_terminal[pane_id].pid = pane_pid @@ -288,16 +303,18 @@ def garbage_collect_panes_result(self, result): continue if removed_pane_ids: + def callback(): for pane_id in removed_pane_ids: terminal = pane_id_to_terminal.pop(pane_id, None) if terminal: terminal.close() return False + GObject.idle_add(callback) def initial_layout_result(self, result): - dbg('checking window layout') # JACK_TEST + dbg("checking window layout") # JACK_TEST window_layouts = [] for line in result: window_layout = line.strip() @@ -310,10 +327,10 @@ def initial_layout_result(self, result): dbg(parsed_layout) window_layouts.extend(layout.parse_layout(parsed_layout[0])) # window_layouts.append(layout.parse_layout(window_layout)) - dbg('window layouts: {}'.format(window_layouts)) # JACK_TEST - terminator_layout = layout.convert_to_terminator_layout( - window_layouts) + dbg("window layouts: {}".format(window_layouts)) # JACK_TEST + terminator_layout = layout.convert_to_terminator_layout(window_layouts) import pprint + dbg(pprint.pformat(terminator_layout)) self.terminator.initial_layout = terminator_layout @@ -321,25 +338,27 @@ def result_callback(self, result, pane_id): terminal = self.terminator.pane_id_to_terminal.get(pane_id) if not terminal: return - output = b'\r\n'.join(l for l in result if l) + output = b"\r\n".join(l for l in result if l) dbg(output) - terminal.vte.feed(output.decode('unicode-escape').encode('latin-1')) + terminal.vte.feed(output.decode("unicode-escape").encode("latin-1")) def initial_output_result_callback(self, pane_id): def result_callback(result): terminal = self.terminator.pane_id_to_terminal.get(pane_id) if not terminal: return - output = '\r\n'.join(l for l in result if l) - terminal.vte.feed(output.decode('string_escape')) + output = "\r\n".join(l for l in result if l) + terminal.vte.feed(output.decode("string_escape")) + return result_callback def terminate(self): def callback(): for window in self.terminator.windows: - window.emit('destroy') + window.emit("destroy") + GObject.idle_add(callback) def noop(*args): - dbg('passed on notification: {}'.format(args)) + dbg("passed on notification: {}".format(args)) From b8299224e7a4757d04e403ff780bff0625d167fa Mon Sep 17 00:00:00 2001 From: Jack Gaino Date: Sun, 21 Mar 2021 10:13:09 -0400 Subject: [PATCH 11/29] Return True after handling keypress Not doing this results in double keypresses --- terminatorlib/terminal.py | 1 + 1 file changed, 1 insertion(+) diff --git a/terminatorlib/terminal.py b/terminatorlib/terminal.py index 13b1fa804..25e871ede 100644 --- a/terminatorlib/terminal.py +++ b/terminatorlib/terminal.py @@ -929,6 +929,7 @@ def on_keypress(self, widget, event): if self.terminator.tmux_control: self.control.send_keypress(event, pane_id=self.pane_id) + return True return False From a5e91a9ac94bab2ffdddd16b923e16eedbeabd8e Mon Sep 17 00:00:00 2001 From: Jack Gaino Date: Sun, 21 Mar 2021 10:14:03 -0400 Subject: [PATCH 12/29] Return True after handling keypress Properly decode everywhere --- terminatorlib/tmux/notifications.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/terminatorlib/tmux/notifications.py b/terminatorlib/tmux/notifications.py index 075022fef..98a61c3d4 100644 --- a/terminatorlib/tmux/notifications.py +++ b/terminatorlib/tmux/notifications.py @@ -76,7 +76,7 @@ class LayoutChange(Notification): def consume(self, line, *args): # attributes not present default to None - line_items = line.split(b" ") + line_items = line.decode('unicode-escape').split(" ") window_id, window_layout, window_visible_layout, window_flags = line_items + [ None ] * (len(self.attributes) - len(line_items)) @@ -96,7 +96,7 @@ def consume(self, line, *args): # pane_id = line[0] # output = ' '.join(line[1:]) pane_id, output = line.split(b" ", 1) - self.pane_id = pane_id + self.pane_id = pane_id.decode('latin-1') self.output = output @@ -107,7 +107,7 @@ class SessionChanged(Notification): attributes = ["session_id", "session_name"] def consume(self, line, *args): - session_id, session_name = line.split(b" ") + session_id, session_name = line.decode('unicode-escape').split(" ") self.session_id = session_id self.session_name = session_name @@ -119,7 +119,7 @@ class SessionRenamed(Notification): attributes = ["session_id", "session_name"] def consume(self, line, *args): - session_id, session_name = line.split(b" ") + session_id, session_name = line.decode('unicode-escape').split(" ") self.session_id = session_id self.session_name = session_name @@ -138,7 +138,7 @@ class UnlinkedWindowAdd(Notification): attributes = ["window_id"] def consume(self, line, *args): - (window_id,) = line.split(b" ") + (window_id,) = line.decode('unicode-escape').split(" ") self.window_id = window_id @@ -149,7 +149,7 @@ class WindowAdd(Notification): attributes = ["window_id"] def consume(self, line, *args): - (window_id,) = line.split(b" ") + (window_id,) = line.decode('unicode-escape').split(" ") self.window_id = window_id @@ -160,7 +160,7 @@ class UnlinkedWindowClose(Notification): attributes = ["window_id"] def consume(self, line, *args): - (window_id,) = line.split(b" ") + (window_id,) = line.decode('unicode-escape').split(" ") self.window_id = window_id @@ -171,7 +171,7 @@ class WindowClose(Notification): attributes = ["window_id"] def consume(self, line, *args): - (window_id,) = line.split(b" ") + (window_id,) = line.decode('unicode-escape').split(" ") self.window_id = window_id @@ -182,7 +182,7 @@ class UnlinkedWindowRenamed(Notification): attributes = ["window_id", "window_name"] def consume(self, line, *args): - window_id, window_name = line.split(b" ") + window_id, window_name = line.decode('unicode-escape').split(" ") self.window_id = window_id self.window_name = window_name @@ -194,7 +194,7 @@ class WindowRenamed(Notification): attributes = ["window_id", "window_name"] def consume(self, line, *args): - window_id, window_name = line.split(b" ") + window_id, window_name = line.decode('unicode-escape').split(" ") self.window_id = window_id self.window_name = window_name @@ -252,7 +252,7 @@ def handle_output(self, notification): output = notification.output dbg(pane_id) dbg(self.terminator.pane_id_to_terminal) - terminal = self.terminator.pane_id_to_terminal.get(pane_id.decode()) + terminal = self.terminator.pane_id_to_terminal.get(pane_id) if not terminal: return for code in ALTERNATE_SCREEN_ENTER_CODES: @@ -266,7 +266,7 @@ def handle_output(self, notification): # figure out the root cause # terminal.vte.feed(output.replace("\033g", "").encode('utf-8')) dbg(output) - terminal.vte.feed(output.decode("unicode-escape").encode("latin-1")) + terminal.vte.feed(output.decode('unicode-escape').encode("latin-1")) def handle_layout_change(self, notification): assert isinstance(notification, LayoutChange) @@ -277,7 +277,7 @@ def handle_window_close(self, notification): GObject.idle_add(self.terminator.tmux_control.garbage_collect_panes) def pane_id_result(self, result): - pane_id, marker = result[0].split(" ") + pane_id, marker = result[0].decode('unicode-escape').split(" ") terminal = self.terminator.find_terminal_by_pane_id(marker) terminal.pane_id = pane_id self.terminator.pane_id_to_terminal[pane_id] = terminal @@ -294,7 +294,7 @@ def garbage_collect_panes_result(self, result): removed_pane_ids = pane_id_to_terminal.keys() for line in result: - pane_id, pane_pid = line.split(b" ") + pane_id, pane_pid = line.decode('unicode-escape').split(" ") try: removed_pane_ids.remove(pane_id) pane_id_to_terminal[pane_id].pid = pane_pid @@ -303,7 +303,6 @@ def garbage_collect_panes_result(self, result): continue if removed_pane_ids: - def callback(): for pane_id in removed_pane_ids: terminal = pane_id_to_terminal.pop(pane_id, None) From 08f27fb9f6afcb259a1e6bdb0d201474aa402472 Mon Sep 17 00:00:00 2001 From: Jack Gaino Date: Sun, 21 Mar 2021 10:14:35 -0400 Subject: [PATCH 13/29] Return True after handling keypress Make a copy of removed pane ID keys, cleanup --- terminatorlib/tmux/notifications.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/terminatorlib/tmux/notifications.py b/terminatorlib/tmux/notifications.py index 98a61c3d4..aa8192b65 100644 --- a/terminatorlib/tmux/notifications.py +++ b/terminatorlib/tmux/notifications.py @@ -291,7 +291,7 @@ def pane_tty_result(self, result): def garbage_collect_panes_result(self, result): pane_id_to_terminal = self.terminator.pane_id_to_terminal - removed_pane_ids = pane_id_to_terminal.keys() + removed_pane_ids = list(pane_id_to_terminal.keys()) for line in result: pane_id, pane_pid = line.decode('unicode-escape').split(" ") @@ -337,20 +337,10 @@ def result_callback(self, result, pane_id): terminal = self.terminator.pane_id_to_terminal.get(pane_id) if not terminal: return - output = b"\r\n".join(l for l in result if l) + output = b"\r\n".join(line for line in result if line) dbg(output) terminal.vte.feed(output.decode("unicode-escape").encode("latin-1")) - def initial_output_result_callback(self, pane_id): - def result_callback(result): - terminal = self.terminator.pane_id_to_terminal.get(pane_id) - if not terminal: - return - output = "\r\n".join(l for l in result if l) - terminal.vte.feed(output.decode("string_escape")) - - return result_callback - def terminate(self): def callback(): for window in self.terminator.windows: From 92f9e27f6a617511d66a611ce7b2732467042050 Mon Sep 17 00:00:00 2001 From: Jack Gaino Date: Sun, 21 Mar 2021 10:14:53 -0400 Subject: [PATCH 14/29] Return True after handling keypress Make PID an integer --- terminatorlib/cwd.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/terminatorlib/cwd.py b/terminatorlib/cwd.py index fed977474..ba0cc6310 100644 --- a/terminatorlib/cwd.py +++ b/terminatorlib/cwd.py @@ -14,6 +14,8 @@ def get_pid_cwd(pid = None): """Determine the cwd of the current process""" + if pid is not None: + pid = int(pid) psinfo = psutil.Process(pid).as_dict() dbg('psinfo: %s %s' % (psinfo['cwd'],psinfo['pid'])) # return func From ab6400d0f155b9537dd0aba72b4c70e5fd88c245 Mon Sep 17 00:00:00 2001 From: Jack Gaino Date: Sun, 21 Mar 2021 10:15:35 -0400 Subject: [PATCH 15/29] Remove Shell forking message when using tmux --- terminatorlib/terminal.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/terminatorlib/terminal.py b/terminatorlib/terminal.py index 25e871ede..e401429e7 100644 --- a/terminatorlib/terminal.py +++ b/terminatorlib/terminal.py @@ -1512,6 +1512,7 @@ def spawn_child(self, widget=None, respawn=False, debugserver=False, dbg('Forking shell: "%s" with args: %s' % (shell, args)) if self.terminator.tmux_control: + dbg('Sending command to a new tmux pane: {}' % args) if self.terminator.initial_layout: pass else: @@ -1523,6 +1524,7 @@ def spawn_child(self, widget=None, respawn=False, debugserver=False, orientation=orientation, pane_id=active_pane_id) else: + dbg('Forking shell: "%s" with args: %s' % (shell, args)) args.insert(0, shell) result, self.pid = self.vte.spawn_sync(Vte.PtyFlags.DEFAULT, self.cwd, From 2f4988bbfe6b3d51e57dd76094d868ad16c067e1 Mon Sep 17 00:00:00 2001 From: Jack Gaino Date: Sun, 21 Mar 2021 18:20:44 -0400 Subject: [PATCH 16/29] Not all notifications have more than one word --- terminatorlib/tmux/control.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/terminatorlib/tmux/control.py b/terminatorlib/tmux/control.py index 362f98bfa..5c86a5c34 100644 --- a/terminatorlib/tmux/control.py +++ b/terminatorlib/tmux/control.py @@ -307,7 +307,10 @@ def consume_notifications(self): dbg("=>>>>> LINE RECEIVED: {}".format(line)) line = line[1:].split(b" ", 1) marker = line[0].decode() - line = line[1] + try: + line = line[1] + except IndexError: + line = b'' # skip MOTD, anything that isn't coming from tmux control mode try: notification = notifications.notifications_mappings[marker]() From 39cd00a05e93d83d39ac3d58bc4a0c161a863c42 Mon Sep 17 00:00:00 2001 From: Jack Gaino Date: Sun, 21 Mar 2021 18:21:48 -0400 Subject: [PATCH 17/29] Rename run_command to spawn_tmux_child --- terminatorlib/terminal.py | 10 +++++----- terminatorlib/tmux/control.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/terminatorlib/terminal.py b/terminatorlib/terminal.py index e401429e7..a0155a4fe 100644 --- a/terminatorlib/terminal.py +++ b/terminatorlib/terminal.py @@ -1518,11 +1518,11 @@ def spawn_child(self, widget=None, respawn=False, debugserver=False, else: command = ' '.join(args) self.pane_id = str(util.make_uuid()) - self.control.run_command(command=command, - cwd=self.cwd, - marker=self.pane_id, - orientation=orientation, - pane_id=active_pane_id) + self.control.spawn_tmux_child(command=command, + cwd=self.cwd, + marker=self.pane_id, + orientation=orientation, + pane_id=active_pane_id) else: dbg('Forking shell: "%s" with args: %s' % (shell, args)) args.insert(0, shell) diff --git a/terminatorlib/tmux/control.py b/terminatorlib/tmux/control.py index 5c86a5c34..e048a63af 100644 --- a/terminatorlib/tmux/control.py +++ b/terminatorlib/tmux/control.py @@ -83,7 +83,7 @@ def run_remote_command(self, popen_command): dbg("Tmux server has gone away.") return - def run_command(self, command, marker, cwd=None, orientation=None, pane_id=None): + def spawn_tmux_child(self, command, marker, cwd=None, orientation=None, pane_id=None): if self.input: if orientation: self.split_window( From bb526b50824a7ba814e6e2b09a13541cf7710b57 Mon Sep 17 00:00:00 2001 From: Jack Gaino Date: Sun, 21 Mar 2021 18:22:44 -0400 Subject: [PATCH 18/29] Disable zooming and terminal drag and drop when in tmux mode --- terminatorlib/terminal.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/terminatorlib/terminal.py b/terminatorlib/terminal.py index a0155a4fe..79affdc58 100644 --- a/terminatorlib/terminal.py +++ b/terminatorlib/terminal.py @@ -1075,6 +1075,10 @@ def on_drag_motion(self, widget, drag_context, x, y, _time, _data): # on self return + if self.terminator.tmux_control: + # Moving the terminals around is not supported in tmux mode + return + alloc = widget.get_allocation() if self.config['use_theme_colors']: @@ -1196,6 +1200,10 @@ def on_drag_data_received(self, widget, drag_context, x, y, selection_data, # The widget argument is actually a Vte.Terminal(). Turn that into a # terminatorlib Terminal() + if self.terminator.tmux_control: + # Moving the terminals around is not supported in tmux mode + return + maker = Factory() while True: widget = widget.get_parent() @@ -1620,10 +1628,16 @@ def feed(self, text): def zoom_in(self): """Increase the font size""" + if self.terminator.tmux_control: + # Zooming causes all kinds of issues when in tmux mode, so we'll just disable it for now + return self.zoom_font(True) def zoom_out(self): """Decrease the font size""" + if self.terminator.tmux_control: + # Zooming causes all kinds of issues when in tmux mode, so we'll just disable it for now + return self.zoom_font(False) def zoom_font(self, zoom_in): @@ -1642,6 +1656,9 @@ def zoom_font(self, zoom_in): def zoom_orig(self): """Restore original font size""" + if self.terminator.tmux_control: + # Zooming causes all kinds of issues when in tmux mode, so we'll just disable it for now + return if self.config['use_system_font']: font = self.config.get_system_mono_font() else: From 167616f12192f0aaa0d3bbc60a0fd97f91d35dea Mon Sep 17 00:00:00 2001 From: Jack Gaino Date: Sun, 21 Mar 2021 18:25:14 -0400 Subject: [PATCH 19/29] Try to account for terminal separators when giving tmux a window size --- terminatorlib/terminal.py | 22 +++++++++++++--------- terminatorlib/util.py | 24 ++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/terminatorlib/terminal.py b/terminatorlib/terminal.py index 79affdc58..7c2a15374 100644 --- a/terminatorlib/terminal.py +++ b/terminatorlib/terminal.py @@ -16,7 +16,8 @@ except ImportError: from urllib import unquote as urlunquote -from .util import dbg, err, spawn_new_terminator, make_uuid, manual_lookup, display_manager, get_column_row_count +from .util import dbg, err, spawn_new_terminator, make_uuid, manual_lookup, display_manager, get_column_row_count, \ + get_amount_of_terminals_in_each_direction from . import util from .config import Config from .cwd import get_pid_cwd @@ -1339,15 +1340,19 @@ def on_vte_size_allocate(self, widget, allocation): row_count = self.vte.get_row_count() self.titlebar.update_terminal_size(column_count, row_count) - if self.terminator.tmux_control: + if self.terminator.tmux_control and not self.terminator.doing_layout: # self.terminator.tmux_control.resize_pane(self.pane_id, row_count, column_count) # FIXME: probably not the best place for this, update tmux client size to match the window geometry - window = self.terminator.get_windows()[0] + window = self.vte.get_toplevel() column_count, row_count = map(int, get_column_row_count(window)) - # dbg("{}::{}: {}x{}".format("NotificationsHandler", "list_panes_size_result", column_count, row_count)) - size_up_to_date = bool(column_count == self.terminator.tmux_control.width and row_count == self.terminator.tmux_control.height) - if not size_up_to_date: - self.terminator.tmux_control.refresh_client(column_count, row_count) + horizontal_terminals, vertical_terminals = get_amount_of_terminals_in_each_direction(window) + if not (column_count == 0 and row_count == 0): + self.terminator.tmux_control.refresh_client(column_count+(horizontal_terminals - 1), row_count + (vertical_terminals - 1)) + self.terminator.tmux_control.resize_pane( + pane_id=self.pane_id, + cols=self.vte.get_column_count(), + rows=self.vte.get_row_count() + ) if self.config['geometry_hinting']: window = self.get_toplevel() @@ -1518,9 +1523,8 @@ def spawn_child(self, widget=None, respawn=False, debugserver=False, if self.terminator.dbus_path: envv.append('TERMINATOR_DBUS_PATH=%s' % self.terminator.dbus_path) - dbg('Forking shell: "%s" with args: %s' % (shell, args)) if self.terminator.tmux_control: - dbg('Sending command to a new tmux pane: {}' % args) + dbg('Spawning a new tmux terminal with args: %s' % args) if self.terminator.initial_layout: pass else: diff --git a/terminatorlib/util.py b/terminatorlib/util.py index 1c8cda331..c91b46948 100644 --- a/terminatorlib/util.py +++ b/terminatorlib/util.py @@ -346,6 +346,30 @@ def get_column_row_count(window): return (column_sum, row_sum) +def get_amount_of_terminals_in_each_direction(window): + base_x = base_y = None + + # NOTE: on Wayland, we cannot assume that the coordinate system + # for our application starts at 0x0, so we try to guess our + # current baseline at runtime + if display_manager() == 'WAYLAND' and not base_x: + base_x, base_y = get_wayland_baseline(window) + else: + base_x = base_y = 0 + + terminals = window.get_visible_terminals() + horizontal_terminals = 0 + vertical_terminals = 0 + for terminal in terminals: + rect = terminal.get_allocation() + + if rect.x <= base_x: + vertical_terminals += 1 + if rect.y <= base_y: + horizontal_terminals += 1 + + return horizontal_terminals, vertical_terminals + def get_wayland_baseline(window): terminals = window.get_visible_terminals() From 87f26970855cf02f5afc77dceb06a73b8353130a Mon Sep 17 00:00:00 2001 From: Jack Gaino Date: Sun, 21 Mar 2021 18:28:59 -0400 Subject: [PATCH 20/29] Lint --- terminatorlib/tmux/notifications.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/terminatorlib/tmux/notifications.py b/terminatorlib/tmux/notifications.py index aa8192b65..e9c23184c 100644 --- a/terminatorlib/tmux/notifications.py +++ b/terminatorlib/tmux/notifications.py @@ -22,7 +22,6 @@ def notification(cls): class Notification(object): - marker = "undefined" attributes = [] @@ -76,7 +75,7 @@ class LayoutChange(Notification): def consume(self, line, *args): # attributes not present default to None - line_items = line.decode('unicode-escape').split(" ") + line_items = line.decode("unicode-escape").split(" ") window_id, window_layout, window_visible_layout, window_flags = line_items + [ None ] * (len(self.attributes) - len(line_items)) @@ -96,7 +95,7 @@ def consume(self, line, *args): # pane_id = line[0] # output = ' '.join(line[1:]) pane_id, output = line.split(b" ", 1) - self.pane_id = pane_id.decode('latin-1') + self.pane_id = pane_id.decode("latin-1") self.output = output @@ -107,7 +106,7 @@ class SessionChanged(Notification): attributes = ["session_id", "session_name"] def consume(self, line, *args): - session_id, session_name = line.decode('unicode-escape').split(" ") + session_id, session_name = line.decode("unicode-escape").split(" ") self.session_id = session_id self.session_name = session_name @@ -119,7 +118,7 @@ class SessionRenamed(Notification): attributes = ["session_id", "session_name"] def consume(self, line, *args): - session_id, session_name = line.decode('unicode-escape').split(" ") + session_id, session_name = line.decode("unicode-escape").split(" ") self.session_id = session_id self.session_name = session_name @@ -138,7 +137,7 @@ class UnlinkedWindowAdd(Notification): attributes = ["window_id"] def consume(self, line, *args): - (window_id,) = line.decode('unicode-escape').split(" ") + (window_id,) = line.decode("unicode-escape").split(" ") self.window_id = window_id @@ -149,7 +148,7 @@ class WindowAdd(Notification): attributes = ["window_id"] def consume(self, line, *args): - (window_id,) = line.decode('unicode-escape').split(" ") + (window_id,) = line.decode("unicode-escape").split(" ") self.window_id = window_id @@ -160,7 +159,7 @@ class UnlinkedWindowClose(Notification): attributes = ["window_id"] def consume(self, line, *args): - (window_id,) = line.decode('unicode-escape').split(" ") + (window_id,) = line.decode("unicode-escape").split(" ") self.window_id = window_id @@ -171,7 +170,7 @@ class WindowClose(Notification): attributes = ["window_id"] def consume(self, line, *args): - (window_id,) = line.decode('unicode-escape').split(" ") + (window_id,) = line.decode("unicode-escape").split(" ") self.window_id = window_id @@ -182,7 +181,7 @@ class UnlinkedWindowRenamed(Notification): attributes = ["window_id", "window_name"] def consume(self, line, *args): - window_id, window_name = line.decode('unicode-escape').split(" ") + window_id, window_name = line.decode("unicode-escape").split(" ") self.window_id = window_id self.window_name = window_name @@ -194,7 +193,7 @@ class WindowRenamed(Notification): attributes = ["window_id", "window_name"] def consume(self, line, *args): - window_id, window_name = line.decode('unicode-escape').split(" ") + window_id, window_name = line.decode("unicode-escape").split(" ") self.window_id = window_id self.window_name = window_name @@ -266,7 +265,7 @@ def handle_output(self, notification): # figure out the root cause # terminal.vte.feed(output.replace("\033g", "").encode('utf-8')) dbg(output) - terminal.vte.feed(output.decode('unicode-escape').encode("latin-1")) + terminal.vte.feed(output.decode("unicode-escape").encode("latin-1")) def handle_layout_change(self, notification): assert isinstance(notification, LayoutChange) @@ -277,7 +276,7 @@ def handle_window_close(self, notification): GObject.idle_add(self.terminator.tmux_control.garbage_collect_panes) def pane_id_result(self, result): - pane_id, marker = result[0].decode('unicode-escape').split(" ") + pane_id, marker = result[0].decode("unicode-escape").split(" ") terminal = self.terminator.find_terminal_by_pane_id(marker) terminal.pane_id = pane_id self.terminator.pane_id_to_terminal[pane_id] = terminal @@ -294,7 +293,7 @@ def garbage_collect_panes_result(self, result): removed_pane_ids = list(pane_id_to_terminal.keys()) for line in result: - pane_id, pane_pid = line.decode('unicode-escape').split(" ") + pane_id, pane_pid = line.decode("unicode-escape").split(" ") try: removed_pane_ids.remove(pane_id) pane_id_to_terminal[pane_id].pid = pane_pid @@ -303,6 +302,7 @@ def garbage_collect_panes_result(self, result): continue if removed_pane_ids: + def callback(): for pane_id in removed_pane_ids: terminal = pane_id_to_terminal.pop(pane_id, None) @@ -338,7 +338,6 @@ def result_callback(self, result, pane_id): if not terminal: return output = b"\r\n".join(line for line in result if line) - dbg(output) terminal.vte.feed(output.decode("unicode-escape").encode("latin-1")) def terminate(self): From cdcdb2dd6aa73c467d0b9d230ff670319e23f58f Mon Sep 17 00:00:00 2001 From: Jack Gaino Date: Sun, 21 Mar 2021 18:29:07 -0400 Subject: [PATCH 21/29] Handle tmux closing a window on us --- terminatorlib/tmux/notifications.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/terminatorlib/tmux/notifications.py b/terminatorlib/tmux/notifications.py index e9c23184c..062983198 100644 --- a/terminatorlib/tmux/notifications.py +++ b/terminatorlib/tmux/notifications.py @@ -275,6 +275,14 @@ def handle_window_close(self, notification): assert isinstance(notification, WindowClose) GObject.idle_add(self.terminator.tmux_control.garbage_collect_panes) + def handle_window_add(self, notification): + assert isinstance(notification, WindowAdd) + GObject.idle_add(self.terminator.tmux_control.garbage_collect_panes) + + def handle_unlinked_window_close(self, notification): + assert isinstance(notification, UnlinkedWindowClose) + GObject.idle_add(self.terminator.tmux_control.garbage_collect_panes) + def pane_id_result(self, result): pane_id, marker = result[0].decode("unicode-escape").split(" ") terminal = self.terminator.find_terminal_by_pane_id(marker) From db6ebdb6cbb8b3a4c7476b669ad0ef9eb010d185 Mon Sep 17 00:00:00 2001 From: Jack Gaino Date: Sun, 21 Mar 2021 18:29:36 -0400 Subject: [PATCH 22/29] Don't determine CWD if using a remote tmux instance --- terminatorlib/terminal.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/terminatorlib/terminal.py b/terminatorlib/terminal.py index 7c2a15374..e69aaaff2 100644 --- a/terminatorlib/terminal.py +++ b/terminatorlib/terminal.py @@ -274,6 +274,9 @@ def switch_to_previous_profile(self): def get_cwd(self): """Return our cwd""" vte_cwd = self.vte.get_current_directory_uri() + if self.terminator.tmux_control and self.terminator.tmux_control.remote is not None: + return None + if vte_cwd: # OSC7 pwd gives an answer return(GLib.filename_from_uri(vte_cwd)[0]) From 71fe7d9b519cf6c07ccf62a88bfa6342572dca4c Mon Sep 17 00:00:00 2001 From: Jack Gaino Date: Sun, 21 Mar 2021 18:29:50 -0400 Subject: [PATCH 23/29] Don't buffer remote connections --- terminatorlib/tmux/control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminatorlib/tmux/control.py b/terminatorlib/tmux/control.py index e048a63af..e3b9a634d 100644 --- a/terminatorlib/tmux/control.py +++ b/terminatorlib/tmux/control.py @@ -64,7 +64,7 @@ def remote_connect(self, command): return popen_command = "ssh " + self.remote self.tmux = subprocess.Popen( - popen_command, stdout=subprocess.PIPE, stdin=subprocess.PIPE, shell=True + popen_command, stdout=subprocess.PIPE, stdin=subprocess.PIPE, shell=True, bufsize=0 ) self.input = self.tmux.stdin self.output = self.tmux.stdout From 67bcb3310696806889bda40f6ee961bfd5352a01 Mon Sep 17 00:00:00 2001 From: Jack Gaino Date: Sun, 21 Mar 2021 18:30:12 -0400 Subject: [PATCH 24/29] Prevent zooming through any means when in tmux mode --- terminatorlib/window.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/terminatorlib/window.py b/terminatorlib/window.py index 7efda19f5..08beba15a 100644 --- a/terminatorlib/window.py +++ b/terminatorlib/window.py @@ -506,6 +506,9 @@ def split_axis(self, widget, vertical=True, cwd=None, sibling=None, widgetfirst= def zoom(self, widget, font_scale=True): """Zoom a terminal widget""" + if self.terminator.tmux_control: + # Zooming causes all kinds of issues when in tmux mode, so we'll just disable it for now + return children = self.get_children() if widget in children: From b0765b8b64187bdb24ab23595bccddb8e8305abe Mon Sep 17 00:00:00 2001 From: Jack Gaino Date: Sun, 21 Mar 2021 18:30:53 -0400 Subject: [PATCH 25/29] Black --- terminatorlib/tmux/control.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/terminatorlib/tmux/control.py b/terminatorlib/tmux/control.py index e3b9a634d..8ed93082f 100644 --- a/terminatorlib/tmux/control.py +++ b/terminatorlib/tmux/control.py @@ -64,7 +64,11 @@ def remote_connect(self, command): return popen_command = "ssh " + self.remote self.tmux = subprocess.Popen( - popen_command, stdout=subprocess.PIPE, stdin=subprocess.PIPE, shell=True, bufsize=0 + popen_command, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + shell=True, + bufsize=0, ) self.input = self.tmux.stdin self.output = self.tmux.stdout @@ -83,7 +87,9 @@ def run_remote_command(self, popen_command): dbg("Tmux server has gone away.") return - def spawn_tmux_child(self, command, marker, cwd=None, orientation=None, pane_id=None): + def spawn_tmux_child( + self, command, marker, cwd=None, orientation=None, pane_id=None + ): if self.input: if orientation: self.split_window( @@ -310,7 +316,7 @@ def consume_notifications(self): try: line = line[1] except IndexError: - line = b'' + line = b"" # skip MOTD, anything that isn't coming from tmux control mode try: notification = notifications.notifications_mappings[marker]() From 1c752334f082bc441016500879b7c278b94bd3a9 Mon Sep 17 00:00:00 2001 From: Jack Gaino Date: Sun, 21 Mar 2021 18:47:56 -0400 Subject: [PATCH 26/29] Clean up some debug messages --- terminatorlib/terminal.py | 1 - terminatorlib/tmux/control.py | 3 +-- terminatorlib/tmux/notifications.py | 35 ++++++++++------------------- 3 files changed, 13 insertions(+), 26 deletions(-) diff --git a/terminatorlib/terminal.py b/terminatorlib/terminal.py index e69aaaff2..558a12b89 100644 --- a/terminatorlib/terminal.py +++ b/terminatorlib/terminal.py @@ -421,7 +421,6 @@ def maybe_copy_clipboard(self): def connect_signals(self): """Connect all the gtk signals and drag-n-drop mechanics""" - self.scrollbar.connect('button-press-event', self.on_buttonpress) self.cnxids.new(self.vte, 'key-press-event', self.on_keypress) diff --git a/terminatorlib/tmux/control.py b/terminatorlib/tmux/control.py index 8ed93082f..325b61fb9 100644 --- a/terminatorlib/tmux/control.py +++ b/terminatorlib/tmux/control.py @@ -192,7 +192,6 @@ def garbage_collect_panes(self): ) def initial_layout(self): - dbg("running initial layout") # JACK_TEST self._run_command( 'list-windows -t {} -F "#{{window_layout}}"'.format(self.session_name), callback=("initial_layout_result",), @@ -321,7 +320,7 @@ def consume_notifications(self): try: notification = notifications.notifications_mappings[marker]() except KeyError: - dbg("Discarding invalid output from the control terminal.") + dbg("Unknown notification.") continue notification.consume(line, self.output) dbg("consumed notification") # JACK_TEST diff --git a/terminatorlib/tmux/notifications.py b/terminatorlib/tmux/notifications.py index 062983198..9c7838c38 100644 --- a/terminatorlib/tmux/notifications.py +++ b/terminatorlib/tmux/notifications.py @@ -205,22 +205,20 @@ def __init__(self, terminator): def handle(self, notification): try: - dbg("looking for method for {}".format(notification.marker)) # JACK_TEST handler_method = getattr( self, "handle_{}".format(notification.marker.replace("-", "_")) ) - handler_method(notification) - except AttributeError as e: # JACK_TEST - dbg( - "------- method for {} NOT FOUND: {}".format(notification.marker, e) - ) # JACK_TEST - pass - except Exception as e: # JACK_TEST - dbg( - "something went wrong while handling {}: {}".format( - notification.marker, e + except AttributeError as e: + dbg("Handler for notification {} not found".format(notification.marker)) + else: + try: + handler_method(notification) + except Exception as e: + dbg( + "something went wrong while handling {}: {}".format( + notification.marker, e + ) ) - ) # JACK_TEST def handle_begin(self, notification): dbg("### {}".format(notification)) @@ -321,20 +319,11 @@ def callback(): GObject.idle_add(callback) def initial_layout_result(self, result): - dbg("checking window layout") # JACK_TEST window_layouts = [] for line in result: window_layout = line.strip() - dbg(window_layout) - try: - parsed_layout = self.layout_parser.parse(window_layout.decode()) - except Exception as e: - dbg(e) - exit(1) - dbg(parsed_layout) - window_layouts.extend(layout.parse_layout(parsed_layout[0])) - # window_layouts.append(layout.parse_layout(window_layout)) - dbg("window layouts: {}".format(window_layouts)) # JACK_TEST + parsed_layout = self.layout_parser.parse(window_layout.decode()) + window_layouts.append(layout.parse_layout(parsed_layout[0])) terminator_layout = layout.convert_to_terminator_layout(window_layouts) import pprint From 358a22338b334267aa47649b04745c4693d51f8a Mon Sep 17 00:00:00 2001 From: Jack Gaino Date: Sun, 21 Mar 2021 18:49:53 -0400 Subject: [PATCH 27/29] Clean up some debug messages --- terminatorlib/tmux/control.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/terminatorlib/tmux/control.py b/terminatorlib/tmux/control.py index 325b61fb9..3b40beaa1 100644 --- a/terminatorlib/tmux/control.py +++ b/terminatorlib/tmux/control.py @@ -323,16 +323,12 @@ def consume_notifications(self): dbg("Unknown notification.") continue notification.consume(line, self.output) - dbg("consumed notification") # JACK_TEST - try: # JACK_TEST + dbg("consumed notification: {}".format(notification)) # JACK_TEST + try: handler.handle(notification) - except Exception as e: # JACK_TEST - dbg( - "UH OH ------------------------------------------------- {}".format( - e - ) - ) # JACK_TEST - dbg("handled notification") # JACK_TEST + except Exception as e: + dbg("Error while handling notification: {}".format(e)) + dbg("handled notification") handler.terminate() def display_pane_tty(self, pane_id): From 9374c3e497b500360f6d0b919e607cfce8901a75 Mon Sep 17 00:00:00 2001 From: Jack Gaino Date: Sun, 21 Mar 2021 19:00:20 -0400 Subject: [PATCH 28/29] Fix tests --- tests/test_tmux.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/test_tmux.py b/tests/test_tmux.py index 1dac378f4..3f7718c42 100644 --- a/tests/test_tmux.py +++ b/tests/test_tmux.py @@ -2,28 +2,30 @@ import os import sys, os.path + sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), ".."))) import unittest from terminatorlib.tmux import notifications -class NotificationsTests(unittest.TestCase): +class NotificationsTests(unittest.TestCase): def test_layout_changed_parsing(self): layouts = [ - 'sum,80x24,0,0,0', - 'sum,80x24,0,0[80x12,0,0,0,80x5,0,13,1,80x2,0,19,2,80x2,0,22,3]', - 'sum,80x24,0,0{40x24,0,0,0,19x24,41,0,1,9x24,61,0,2,4x24,71,0,3,4x24,76,0,4}', - 'sum,80x24,0,0[80x12,0,0,0,80x5,0,13,1,80x5,0,19{40x5,0,19,2,19x5,41,19,3,9x5,61,19,4,4x5,71,19,5,4x5,76,19,6}]' + "sum,80x24,0,0,0", + "sum,80x24,0,0[80x12,0,0,0,80x5,0,13,1,80x2,0,19,2,80x2,0,22,3]", + "sum,80x24,0,0{40x24,0,0,0,19x24,41,0,1,9x24,61,0,2,4x24,71,0,3,4x24,76,0,4}", + "sum,80x24,0,0[80x12,0,0,0,80x5,0,13,1,80x5,0,19{40x5,0,19,2,19x5,41,19,3,9x5,61,19,4,4x5,71,19,5,4x5,76,19,6}]", ] for layout in layouts: notification = notifications.LayoutChange() - notification.consume(['', layout]) - print notification.window_layout + notification.consume(["", layout]) + print(notification.window_layout) def main(): unittest.main() -if __name__ == '__main__': + +if __name__ == "__main__": main() From 94ad8e0ebf00af798907570d6f63ba16df11d3a2 Mon Sep 17 00:00:00 2001 From: Jack Gaino Date: Mon, 22 Mar 2021 09:30:21 -0400 Subject: [PATCH 29/29] Actually fix test --- tests/test_tmux.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_tmux.py b/tests/test_tmux.py index 3f7718c42..456e9139c 100644 --- a/tests/test_tmux.py +++ b/tests/test_tmux.py @@ -12,14 +12,14 @@ class NotificationsTests(unittest.TestCase): def test_layout_changed_parsing(self): layouts = [ - "sum,80x24,0,0,0", - "sum,80x24,0,0[80x12,0,0,0,80x5,0,13,1,80x2,0,19,2,80x2,0,22,3]", - "sum,80x24,0,0{40x24,0,0,0,19x24,41,0,1,9x24,61,0,2,4x24,71,0,3,4x24,76,0,4}", - "sum,80x24,0,0[80x12,0,0,0,80x5,0,13,1,80x5,0,19{40x5,0,19,2,19x5,41,19,3,9x5,61,19,4,4x5,71,19,5,4x5,76,19,6}]", + b"sum,80x24,0,0,0", + b"sum,80x24,0,0[80x12,0,0,0,80x5,0,13,1,80x2,0,19,2,80x2,0,22,3]", + b"sum,80x24,0,0{40x24,0,0,0,19x24,41,0,1,9x24,61,0,2,4x24,71,0,3,4x24,76,0,4}", + b"sum,80x24,0,0[80x12,0,0,0,80x5,0,13,1,80x5,0,19{40x5,0,19,2,19x5,41,19,3,9x5,61,19,4,4x5,71,19,5,4x5,76,19,6}]", ] for layout in layouts: notification = notifications.LayoutChange() - notification.consume(["", layout]) + notification.consume(b"fake " + layout) print(notification.window_layout)