# -*- coding: utf-8 -*- """Utility and helper methods for tmuxp. tmuxp.util ~~~~~~~~~~ """ from __future__ import absolute_import, division, print_function, \ with_statement, unicode_literals import unittest import collections import subprocess import re import os import sys import logging from distutils.version import StrictVersion from . import exc from ._compat import console_to_str logger = logging.getLogger(__name__) PY2 = sys.version_info[0] == 2 def run_before_script(script_file): """Function to wrap try/except for subprocess.check_call().""" try: proc = subprocess.Popen( script_file, stderr=subprocess.PIPE ) proc.wait() if proc.returncode: stderr = proc.stderr.read() proc.stderr.close() stderr = console_to_str(stderr).split('\n') stderr = '\n'.join(list(filter(None, stderr))) # filter empty values raise exc.BeforeLoadScriptError(proc.returncode, os.path.abspath(script_file), stderr) return proc.returncode except OSError as e: if e.errno == 2: raise exc.BeforeLoadScriptNotExists(e, os.path.abspath(script_file)) else: raise(e) class tmux(object): """:py:mod:`subprocess` for :term:`tmux(1)`. Usage:: proc = tmux('new-session', '-s%' % 'my session') if proc.stderr: raise exc.TmuxpException( 'Command: %s returned error: %s' % (proc.cmd, proc.stderr) ) print('tmux command returned %s' % proc.stdout) Equivalent to: .. code-block:: bash $ tmux new-session -s my session """ def __init__(self, *args, **kwargs): cmd = [which('tmux')] cmd += args # add the command arguments to cmd cmd = [str(c) for c in cmd] self.cmd = cmd try: self.process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) self.process.wait() stdout = self.process.stdout.read() self.process.stdout.close() stderr = self.process.stderr.read() self.process.stderr.close() except Exception as e: logger.error( 'Exception for %s: \n%s' % ( subprocess.list2cmdline(cmd), e ) ) self.stdout = console_to_str(stdout) self.stdout = self.stdout.split('\n') self.stdout = list(filter(None, self.stdout)) # filter empty values self.stderr = console_to_str(stderr) self.stderr = self.stderr.split('\n') self.stderr = list(filter(None, self.stderr)) # filter empty values if 'has-session' in cmd and len(self.stderr): if not self.stdout: self.stdout = self.stderr[0] logging.debug('self.stdout for %s: \n%s' % (' '.join(cmd), self.stdout)) class TmuxMappingObject(collections.MutableMapping): """Base: :py:class:`collections.MutableMapping`. Convenience container. Base class for :class:`Pane`, :class:`Window`, :class:`Session` and :class:`Server`. Instance attributes for useful information :term:`tmux(1)` uses for Session, Window, Pane, stored :attr:`self._TMUX`. For example, a :class:`Window` will have a ``window_id`` and ``window_name``. """ def __getitem__(self, key): return self._TMUX[key] def __setitem__(self, key, value): self._TMUX[key] = value self.dirty = True def __delitem__(self, key): del self._TMUX[key] self.dirty = True def keys(self): """Return list of keys.""" return self._TMUX.keys() def __iter__(self): return self._TMUX.__iter__() def __len__(self): return len(self._TMUX.keys()) class TmuxRelationalObject(object): """Base Class for managing tmux object child entities. Manages collection of child objects (a :class:`Server` has a collection of :class:`Session` objects, a :class:`Session` has collection of :class:`Window`) Children of :class:`TmuxRelationalObject` are going to have a ``self.children``, ``self.childIdAttribute`` and ``self.list_children``. ================ ================== ===================== ============================ Object ``.children`` ``.childIdAttribute`` method ================ ================== ===================== ============================ :class:`Server` ``self._sessions`` 'session_id' :meth:`Server.list_sessions` :class:`Session` ``self._windows`` 'window_id' :meth:`Session.list_windows` :class:`Window` ``self._panes`` 'pane_id' :meth:`Window.list_panes` :class:`Pane` ================ ================== ===================== ============================ """ def findWhere(self, attrs): """Return object on first match. Based on `.findWhere()`_ from `underscore.js`_. .. _.findWhere(): http://underscorejs.org/#findWhere .. _underscore.js: http://underscorejs.org/ """ try: return self.where(attrs)[0] except IndexError: return None def where(self, attrs, first=False): """Return objects matching child objects properties. Based on `.where()`_ from `underscore.js`_. .. _.where(): http://underscorejs.org/#where .. _underscore.js: http://underscorejs.org/ :param attrs: tmux properties to match :type attrs: dict :rtype: list """ # from https://github.com/serkanyersen/underscore.py def by(val, *args): for key, value in attrs.items(): try: if attrs[key] != val[key]: return False except KeyError: return False return True if first: return list(filter(by, self.children))[0] else: return list(filter(by, self.children)) def getById(self, id): """Return object based on ``childIdAttribute``. Based on `.get()`_ from `backbone.js`_. .. _backbone.js: http://backbonejs.org/ .. _.get(): http://backbonejs.org/#Collection-get :param id: :type id: string :rtype: object """ for child in self.children: if child[self.childIdAttribute] == id: return child else: continue return None def which(exe=None): """Return path of bin. Python clone of /usr/bin/which. from salt.util - https://www.github.com/saltstack/salt - license apache :param exe: Application to search PATHs for. :type exe: string :rtype: string """ if exe: if os.access(exe, os.X_OK): return exe # default path based on busybox's default default_path = '/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin' search_path = os.environ.get('PATH', default_path) for path in search_path.split(os.pathsep): full_path = os.path.join(path, exe) if os.access(full_path, os.X_OK): return full_path raise exc.TmuxpException( '{0!r} could not be found in the following search ' 'path: {1!r}'.format( exe, search_path ) ) logger.error('No executable was passed to be searched by which') return None def is_version(version): """Return True if tmux version installed. :param version: version, '1.8' :param type: string :rtype: bool """ proc = tmux('-V') if proc.stderr: raise exc.TmuxpException(proc.stderr) installed_version = proc.stdout[0].split('tmux ')[1] return StrictVersion(installed_version) == StrictVersion(version) def has_required_tmux_version(version=None): """Return if tmux meets version requirement. Version >1.8 or above. :versionchanged: 0.1.7 Versions will now remove trailing letters per `Issue 55`_. .. _Issue 55: https://github.com/tony/tmuxp/issues/55. """ if not version: proc = tmux('-V') if proc.stderr: raise exc.TmuxpException(proc.stderr) version = proc.stdout[0].split('tmux ')[1] version = re.sub(r'[a-z]', '', version) if StrictVersion(version) <= StrictVersion("1.7"): raise exc.TmuxpException( 'tmuxp only supports tmux 1.8 and greater. This system' ' has %s installed. Upgrade your tmux to use tmuxp.' % version ) return version def oh_my_zsh_auto_title(): """Give warning and offer to fix ``DISABLE_AUTO_TITLE``. see: https://github.com/robbyrussell/oh-my-zsh/pull/257 """ if 'SHELL' in os.environ and 'zsh' in os.environ.get('SHELL'): if os.path.exists(os.path.expanduser('~/.oh-my-zsh')): # oh-my-zsh exists if ( 'DISABLE_AUTO_TITLE' not in os.environ or os.environ.get('DISABLE_AUTO_TITLE') == "false" ): print('Please set:\n\n' '\texport DISABLE_AUTO_TITLE = \'true\'\n\n' 'in ~/.zshrc or where your zsh profile is stored.\n' 'Remember the "export" at the beginning!\n\n' 'Then create a new shell or type:\n\n' '\t$ source ~/.zshrc')