diff --git a/CHANGELOG b/CHANGELOG index 7faa634..9f7bfc3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,88 @@ CHANGELOG ========= +0.14: 2017-07-27 +---------------- + +Fixes: +- Fixed bug in remove_reader (ValueError). +- Pin Pyte requirements belowe 0.6.0. + + +0.13: 2016-10-16 +---------------- + +New features: +- Added status-interval option. +- Support for ANSI colors only. + * Added --ansicolor option. + * Check PROMPT_TOOLKIT_ANSI_COLORS_ONLY environment variable. +- Added pane-status option for hiding the pane status bar. + (Disabled by default.) +- Expose shift+arrow keys for key bindings. + +Performance improvements: +- Only do datetime formatting if the string actually contains a '%' + character. + +Fixes: +- Catch OSError in os.tcgetpgrp. +- Clean up sockets, also in case of a crash. +- Fix in process.py: don't call remove_reader when master is None. + + +0.12: 2016-08-03 +---------------- + +Fixes: +- Prompt_toolkit 1.0.4 compatibilty. +- Python 2.6 compatibility. + + +0.11: 2016-06-27 +---------------- + +Fixes: +- Fix for OS X El Capitan: LoadLibrary('libc.dylib') failed. +- Compatibility with the latest prompt_toolkit. + + +0.10: 2016-05-05 +---------------- + +Upgrade to prompt_toolkit 1.0.0 + +New features: +- Added 'C-b PPage' key binding (like tmux). +- Many performance improvements in the vt100 parser. + +Improvements/fixes: +- Don't crash when decoding utf-8 input fails. (Sometimes it happens when using + the mouse in lxterminal.) +- Cleanup CLI object when the client was detached. (The server would become + very slow if the CLI was not removed for a couple of times.) +- Replace errors when decoding utf-8 input. +- Fixes regarding multiwidth characters. +- Bugfix: Don't use 'del' on a defaultdict, but use pop(..., None) instead, in + order to avoid key errors. +- Handle decomposed unicode characters correctly. +- Bugfix regarding the handling of 'clear'. +- Fixes a bug where the cursor stays at the top. +- Fix: The socket in the pymux client should be blocking. + + +0.9: 2016-03-14 +--------------- + +Upgrade to prompt_toolkit 0.60 + + +0.8: 2016-03-06 +--------------- + +Upgrade to prompt_toolkit 0.59 + + 0.7: 2016-01-16 --------------- diff --git a/README.rst b/README.rst index 7d71bd3..8212ec0 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,13 @@ Pymux ===== +WARNING: This project requires maintenance. The current master branch requires +an old version of both prompt_toolkit and ptterm. There is a prompt-toolkit-3.0 +branch here that is compatible with the latest prompt_toolkit and the latest +commit of the master branch of ptterm, but for that branch, only `pymux +standalone` is working at the moment. + + *A terminal multiplexer (like tmux) in Python* :: diff --git a/pymux/arrangement.py b/pymux/arrangement.py index bafac42..7c3ef58 100644 --- a/pymux/arrangement.py +++ b/pymux/arrangement.py @@ -8,13 +8,10 @@ arranged by ordering them in HSplit/VSplit instances. """ from __future__ import unicode_literals -from .process import Process +from ptterm import Terminal +from prompt_toolkit.application.current import get_app, set_app from prompt_toolkit.buffer import Buffer -from prompt_toolkit.document import Document -from prompt_toolkit.interface import CommandLineInterface -from prompt_toolkit.search_state import SearchState -from prompt_toolkit.token import Token import math import os @@ -50,10 +47,10 @@ class Pane(object): """ _pane_counter = 1000 # Start at 1000, to be sure to not confuse this with pane indexes. - def __init__(self, process): - assert isinstance(process, Process) + def __init__(self, terminal=None): + assert isinstance(terminal, Terminal) - self.process = process + self.terminal = terminal self.chosen_name = None # Displayed the clock instead of this pane content. @@ -73,10 +70,9 @@ def __init__(self, process): self.display_scroll_buffer = False self.scroll_buffer_title = '' - # Search buffer, for use in copy mode. (Each pane gets its own search buffer.) - self.search_buffer = Buffer() - self.is_searching = False - self.search_state = SearchState(ignore_case=False) + @property + def process(self): + return self.terminal.process @property def name(self): @@ -99,41 +95,13 @@ def enter_copy_mode(self): Suspend the process, and copy the screen content to the `scroll_buffer`. That way the user can search through the history and copy/paste. """ - document, get_tokens_for_line = self.process.create_copy_document() - self._enter_scroll_buffer('Copy', document, get_tokens_for_line) - - def display_text(self, text, title=''): - """ - Display the given text in the scroll buffer. - """ - document = Document(text, 0) - - def get_tokens_for_line(lineno): - return [(Token, document.lines[lineno])] - - self._enter_scroll_buffer( - title, - document=document, - get_tokens_for_line=get_tokens_for_line) - - def _enter_scroll_buffer(self, title, document, get_tokens_for_line): - # Suspend child process. - self.process.suspend() + self.terminal.enter_copy_mode() - self.scroll_buffer.set_document(document, bypass_readonly=True) - self.copy_get_tokens_for_line = get_tokens_for_line - self.display_scroll_buffer = True - self.scroll_buffer_title = title - - # Reset search state. - self.search_state = SearchState(ignore_case=False) - - def exit_scroll_buffer(self): + def focus(self): """ - Exit scroll buffer. (Exits help or copy mode.) + Focus this pane. """ - self.process.resume() - self.display_scroll_buffer = False + get_app().layout.focus(self.terminal) class _WeightsDictionary(weakref.WeakKeyDictionary): @@ -210,6 +178,9 @@ def invalidation_hash(self): Return a hash (string) that can be used to determine when the layout has to be rebuild. """ +# if not self.root: +# return '' + def _hash_for_split(split): result = [] for item in split: @@ -382,7 +353,10 @@ def active_process(self): def focus_next(self, count=1): " Focus the next pane. " panes = self.panes - self.active_pane = panes[(panes.index(self.active_pane) + count) % len(panes)] + if panes: + self.active_pane = panes[(panes.index(self.active_pane) + count) % len(panes)] + else: + self.active_pane = None # No panes left. def focus_previous(self): " Focus the previous pane. " @@ -590,65 +564,54 @@ def __init__(self): # is attached. self._last_active_window = None - def pane_has_priority(self, pane): - """ - Return True when this Pane sohuld get priority in the output processing. - This is true for panes that have the focus in any of the visible windows. - """ - windows = set(self._active_window_for_cli.values()) - - for w in windows: - if w.active_pane == pane: - return True - - return False - - def invalidation_hash(self, cli): + def invalidation_hash(self): """ When this changes, the layout needs to be rebuild. """ - w = self.get_active_window(cli) + if not self.windows: + return '' + + w = self.get_active_window() return w.invalidation_hash() - def get_active_window(self, cli): + def get_active_window(self): """ The current active :class:`.Window`. """ - assert isinstance(cli, CommandLineInterface) + app = get_app() try: - return self._active_window_for_cli[cli] + return self._active_window_for_cli[app] except KeyError: - self._active_window_for_cli[cli] = self._last_active_window or self.windows[0] + self._active_window_for_cli[app] = self._last_active_window or self.windows[0] return self.windows[0] - def set_active_window(self, cli, window): - assert isinstance(cli, CommandLineInterface) + def set_active_window(self, window): assert isinstance(window, Window) + app = get_app() - previous = self.get_active_window(cli) - self._prev_active_window_for_cli[cli] = previous - self._active_window_for_cli[cli] = window + previous = self.get_active_window() + self._prev_active_window_for_cli[app] = previous + self._active_window_for_cli[app] = window self._last_active_window = window - def set_active_window_from_pane_id(self, cli, pane_id): + def set_active_window_from_pane_id(self, pane_id): """ Make the window with this pane ID the active Window. """ - assert isinstance(cli, CommandLineInterface) assert isinstance(pane_id, int) for w in self.windows: for p in w.panes: if p.pane_id == pane_id: - self.set_active_window(cli, w) + self.set_active_window(w) - def get_previous_active_window(self, cli): + def get_previous_active_window(self): " The previous active Window or None if unknown. " - assert isinstance(cli, CommandLineInterface) + app = get_app() try: - return self._prev_active_window_for_cli[cli] + return self._prev_active_window_for_cli[app] except KeyError: return None @@ -658,17 +621,15 @@ def get_window_by_index(self, index): if w.index == index: return w - def create_window(self, cli, pane, name=None, set_active=True): + def create_window(self, pane, name=None, set_active=True): """ Create a new window that contains just this pane. - :param cli: If been given, this window will be focussed for that client. :param pane: The :class:`.Pane` instance to put in the new window. :param name: If given, name for the new window. :param set_active: When True, focus the new window. """ assert isinstance(pane, Pane) - assert cli is None or isinstance(cli, CommandLineInterface) assert name is None or isinstance(name, six.text_type) # Take the first available index. @@ -686,8 +647,10 @@ def create_window(self, cli, pane, name=None, set_active=True): # Sort windows by index. self.windows = sorted(self.windows, key=lambda w: w.index) - if cli is not None and set_active: - self.set_active_window(cli, w) + app = get_app(return_none=True) + + if app is not None and set_active: + self.set_active_window(w) if name is not None: w.chosen_name = name @@ -707,13 +670,11 @@ def move_window(self, window, new_index): # Sort windows by index. self.windows = sorted(self.windows, key=lambda w: w.index) - def get_active_pane(self, cli): + def get_active_pane(self): """ The current :class:`.Pane` from the current window. """ - assert isinstance(cli, CommandLineInterface) - - w = self.get_active_window(cli) + w = self.get_active_window() if w is not None: return w.active_pane @@ -729,49 +690,42 @@ def remove_pane(self, pane): # No panes left in this window? if not w.has_panes: # Focus next. - for cli, active_w in self._active_window_for_cli.items(): + for app, active_w in self._active_window_for_cli.items(): if w == active_w: - self.focus_next_window(cli) + with set_app(app): + self.focus_next_window() self.windows.remove(w) - def focus_previous_window(self, cli): - assert isinstance(cli, CommandLineInterface) - - w = self.get_active_window(cli) + def focus_previous_window(self): + w = self.get_active_window() - self.set_active_window(cli, self.windows[ + self.set_active_window(self.windows[ (self.windows.index(w) - 1) % len(self.windows)]) - def focus_next_window(self, cli): - assert isinstance(cli, CommandLineInterface) + def focus_next_window(self): + w = self.get_active_window() - w = self.get_active_window(cli) - - self.set_active_window(cli, self.windows[ + self.set_active_window(self.windows[ (self.windows.index(w) + 1) % len(self.windows)]) - def break_pane(self, cli, set_active=True): + def break_pane(self, set_active=True): """ When the current window has multiple panes, remove the pane from this window and put it in a new window. :param set_active: When True, focus the new window. """ - assert isinstance(cli, CommandLineInterface) - - w = self.get_active_window(cli) + w = self.get_active_window() if len(w.panes) > 1: pane = w.active_pane - self.get_active_window(cli).remove_pane(pane) - self.create_window(cli, pane, set_active=set_active) + self.get_active_window().remove_pane(pane) + self.create_window(pane, set_active=set_active) - def rotate_window(self, cli, count=1): + def rotate_window(self, count=1): " Rotate the panes in the active window. " - assert isinstance(cli, CommandLineInterface) - - w = self.get_active_window(cli) + w = self.get_active_window() w.rotate(count=count) @property diff --git a/pymux/client/__init__.py b/pymux/client/__init__.py new file mode 100644 index 0000000..dacbf9e --- /dev/null +++ b/pymux/client/__init__.py @@ -0,0 +1,3 @@ +from __future__ import unicode_literals +from .base import Client +from .defaults import create_client, list_clients diff --git a/pymux/client/base.py b/pymux/client/base.py new file mode 100644 index 0000000..8443360 --- /dev/null +++ b/pymux/client/base.py @@ -0,0 +1,22 @@ +from __future__ import unicode_literals + +from prompt_toolkit.output import ColorDepth +from abc import ABCMeta +from six import with_metaclass + + +__all__ = [ + 'Client', +] + + +class Client(with_metaclass(ABCMeta, object)): + def run_command(self, command, pane_id=None): + """ + Ask the server to run this command. + """ + + def attach(self, detach_other_clients=False, color_depth=ColorDepth.DEPTH_8_BIT): + """ + Attach client user interface. + """ diff --git a/pymux/client/defaults.py b/pymux/client/defaults.py new file mode 100644 index 0000000..929e874 --- /dev/null +++ b/pymux/client/defaults.py @@ -0,0 +1,24 @@ +from __future__ import unicode_literals +from prompt_toolkit.utils import is_windows +__all__ = [ + 'create_client', + 'list_clients', +] + + +def create_client(socket_name): + if is_windows(): + from .windows import WindowsClient + return WindowsClient(socket_name) + else: + from .posix import PosixClient + return PosixClient(socket_name) + + +def list_clients(): + if is_windows(): + from .windows import list_clients + return list_clients() + else: + from .posix import list_clients + return list_clients() diff --git a/pymux/client.py b/pymux/client/posix.py similarity index 69% rename from pymux/client.py rename to pymux/client/posix.py index 1431092..53b8662 100644 --- a/pymux/client.py +++ b/pymux/client/posix.py @@ -1,9 +1,10 @@ from __future__ import unicode_literals -from prompt_toolkit.terminal.vt100_input import raw_mode, cooked_mode -from prompt_toolkit.eventloop.posix import _select, call_on_sigwinch -from prompt_toolkit.eventloop.base import INPUT_TIMEOUT -from prompt_toolkit.terminal.vt100_output import _get_size, Vt100_Output +from prompt_toolkit.eventloop.select import select_fds +from prompt_toolkit.input.posix_utils import PosixStdinReader +from prompt_toolkit.input.vt100 import raw_mode, cooked_mode +from prompt_toolkit.output.vt100 import _get_size, Vt100_Output +from prompt_toolkit.output import ColorDepth from pymux.utils import nonblocking @@ -15,15 +16,17 @@ import socket import sys import tempfile +from .base import Client +INPUT_TIMEOUT = .5 __all__ = ( - 'Client', + 'PosixClient', 'list_clients', ) -class Client(object): +class PosixClient(Client): def __init__(self, socket_name): self.socket_name = socket_name self._mode_context_managers = [] @@ -31,7 +34,19 @@ def __init__(self, socket_name): # Connect to socket. self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.socket.connect(socket_name) - self.socket.setblocking(0) + self.socket.setblocking(1) + + # Input reader. + # Some terminals, like lxterminal send non UTF-8 input sequences, + # even when the input encoding is supposed to be UTF-8. This + # happens in the case of mouse clicks in the right area of a wide + # terminal. Apparently, these are some binary blobs in between the + # UTF-8 input.) + # We should not replace these, because this would break the + # decoding otherwise. (Also don't pass errors='ignore', because + # that doesn't work for parsing mouse input escape sequences, which + # consist of a fixed number of bytes.) + self._stdin_reader = PosixStdinReader(sys.stdin.fileno(), errors='replace') def run_command(self, command, pane_id=None): """ @@ -45,18 +60,17 @@ def run_command(self, command, pane_id=None): 'pane_id': pane_id }) - def attach(self, detach_other_clients=False, true_color=False): + def attach(self, detach_other_clients=False, color_depth=ColorDepth.DEPTH_8_BIT): """ Attach client user interface. """ assert isinstance(detach_other_clients, bool) - assert isinstance(true_color, bool) self._send_size() self._send_packet({ 'cmd': 'start-gui', 'detach-others': detach_other_clients, - 'true-color': true_color, + 'color-depth': color_depth, 'term': os.environ.get('TERM', ''), 'data': '' }) @@ -68,9 +82,13 @@ def attach(self, detach_other_clients=False, true_color=False): socket_fd = self.socket.fileno() current_timeout = INPUT_TIMEOUT # Timeout, used to flush escape sequences. - with call_on_sigwinch(self._send_size): + try: + def winch_handler(signum, frame): + self._send_size() + + signal.signal(signal.SIGWINCH, winch_handler) while True: - r, w, x = _select([stdin_fd, socket_fd], [], [], current_timeout) + r = select_fds([stdin_fd, socket_fd], current_timeout) if socket_fd in r: # Received packet from server. @@ -103,6 +121,8 @@ def attach(self, detach_other_clients=False, true_color=False): # Timeout. (Tell the server to flush the vt100 Escape.) self._send_packet({'cmd': 'flush-input'}) current_timeout = None + finally: + signal.signal(signal.SIGWINCH, signal.SIG_IGN) def _process(self, data_buffer): """ @@ -142,17 +162,24 @@ def _process_stdin(self): Received data on stdin. Read and send to server. """ with nonblocking(sys.stdin.fileno()): - data = sys.stdin.read() + data = self._stdin_reader.read() - self._send_packet({ - 'cmd': 'in', - 'data': data, - }) + # Send input in chunks of 4k. + step = 4056 + for i in range(0, len(data), step): + self._send_packet({ + 'cmd': 'in', + 'data': data[i:i + step], + }) def _send_packet(self, data): " Send to server. " data = json.dumps(data).encode('utf-8') + # Be sure that our socket is blocking, otherwise, the send() call could + # raise `BlockingIOError` if the buffer is full. + self.socket.setblocking(1) + self.socket.send(data + b'\0') def _send_size(self): @@ -171,6 +198,6 @@ def list_clients(): p = '%s/pymux.sock.%s.*' % (tempfile.gettempdir(), getpass.getuser()) for path in glob.glob(p): try: - yield Client(path) + yield PosixClient(path) except socket.error: pass diff --git a/pymux/client/windows.py b/pymux/client/windows.py new file mode 100644 index 0000000..c22ab7d --- /dev/null +++ b/pymux/client/windows.py @@ -0,0 +1,128 @@ +from __future__ import unicode_literals + +from ctypes import byref, windll +from ctypes.wintypes import DWORD +from prompt_toolkit.eventloop import ensure_future, From +from prompt_toolkit.eventloop import get_event_loop +from prompt_toolkit.input.win32 import Win32Input +from prompt_toolkit.output import ColorDepth +from prompt_toolkit.output.win32 import Win32Output +from prompt_toolkit.win32_types import STD_OUTPUT_HANDLE +import json +import os +import sys + +from ..pipes.win32_client import PipeClient +from .base import Client + +__all__ = [ + 'WindowsClient', + 'list_clients', +] + +# See: https://msdn.microsoft.com/pl-pl/library/windows/desktop/ms686033(v=vs.85).aspx +ENABLE_PROCESSED_INPUT = 0x0001 +ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 + + +class WindowsClient(Client): + def __init__(self, pipe_name): + self._input = Win32Input() + self._hconsole = windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE) + self._data_buffer = b'' + + self.pipe = PipeClient(pipe_name) + + def attach(self, detach_other_clients=False, color_depth=ColorDepth.DEPTH_8_BIT): + assert isinstance(detach_other_clients, bool) + self._send_size() + self._send_packet({ + 'cmd': 'start-gui', + 'detach-others': detach_other_clients, + 'color-depth': color_depth, + 'term': os.environ.get('TERM', ''), + 'data': '' + }) + + f = ensure_future(self._start_reader()) + with self._input.attach(self._input_ready): + # Run as long as we have a connection with the server. + get_event_loop().run_until_complete(f) # Run forever. + + def _start_reader(self): + """ + Read messages from the Win32 pipe server and handle them. + """ + while True: + message = yield From(self.pipe.read_message()) + self._process(message) + + def _process(self, data_buffer): + """ + Handle incoming packet from server. + """ + packet = json.loads(data_buffer) + + if packet['cmd'] == 'out': + # Call os.write manually. In Python2.6, sys.stdout.write doesn't use UTF-8. + original_mode = DWORD(0) + windll.kernel32.GetConsoleMode(self._hconsole, byref(original_mode)) + + windll.kernel32.SetConsoleMode(self._hconsole, DWORD( + ENABLE_PROCESSED_INPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING)) + + try: + os.write(sys.stdout.fileno(), packet['data'].encode('utf-8')) + finally: + windll.kernel32.SetConsoleMode(self._hconsole, original_mode) + + elif packet['cmd'] == 'suspend': + # Suspend client process to background. + pass + + elif packet['cmd'] == 'mode': + pass + + # # Set terminal to raw/cooked. + # action = packet['data'] + + # if action == 'raw': + # cm = raw_mode(sys.stdin.fileno()) + # cm.__enter__() + # self._mode_context_managers.append(cm) + + # elif action == 'cooked': + # cm = cooked_mode(sys.stdin.fileno()) + # cm.__enter__() + # self._mode_context_managers.append(cm) + + # elif action == 'restore' and self._mode_context_managers: + # cm = self._mode_context_managers.pop() + # cm.__exit__() + + def _input_ready(self): + keys = self._input.read_keys() + if keys: + self._send_packet({ + 'cmd': 'in', + 'data': ''.join(key_press.data for key_press in keys), + }) + + def _send_packet(self, data): + " Send to server. " + data = json.dumps(data) + ensure_future(self.pipe.write_message(data)) + + def _send_size(self): + " Report terminal size to server. " + output = Win32Output(sys.stdout) + rows, cols = output.get_size() + + self._send_packet({ + 'cmd': 'size', + 'data': [rows, cols] + }) + + +def list_clients(): + return [] diff --git a/pymux/commands/commands.py b/pymux/commands/commands.py index 1fca5db..6169aad 100644 --- a/pymux/commands/commands.py +++ b/pymux/commands/commands.py @@ -5,14 +5,13 @@ import shlex import six +from prompt_toolkit.application.current import get_app from prompt_toolkit.document import Document -from prompt_toolkit.enums import SEARCH_BUFFER from prompt_toolkit.key_binding.vi_state import InputMode from pymux.arrangement import LayoutTypes from pymux.commands.aliases import ALIASES from pymux.commands.utils import wrap_argument -from pymux.enums import PROMPT from pymux.format import format_pymux_string from pymux.key_mappings import pymux_key_to_prompt_toolkit_key_sequence, prompt_toolkit_key_to_vt100_key from pymux.layout import focus_right, focus_left, focus_up, focus_down @@ -48,7 +47,7 @@ def get_option_flags_for_command(command): return COMMANDS_TO_OPTION_FLAGS.get(command, []) -def handle_command(pymux, cli, input_string): +def handle_command(pymux, input_string): """ Handle command. """ @@ -68,12 +67,12 @@ def handle_command(pymux, cli, input_string): parts = shlex.split(input_string) except ValueError as e: # E.g. missing closing quote. - pymux.show_message(cli, 'Invalid command %s: %s' % (input_string, e)) + pymux.show_message('Invalid command %s: %s' % (input_string, e)) else: - call_command_handler(parts[0], pymux, cli, parts[1:]) + call_command_handler(parts[0], pymux, parts[1:]) -def call_command_handler(command, pymux, cli, arguments): +def call_command_handler(command, pymux, arguments): """ Execute command. @@ -87,19 +86,19 @@ def call_command_handler(command, pymux, cli, arguments): try: handler = COMMANDS_TO_HANDLERS[command] except KeyError: - pymux.show_message(cli, 'Invalid command: %s' % (command,)) + pymux.show_message('Invalid command: %s' % (command,)) else: try: - handler(pymux, cli, arguments) + handler(pymux, arguments) except CommandException as e: - pymux.show_message(cli, e.message) + pymux.show_message(e.message) def cmd(name, options=''): """ Decorator for all commands. - Commands will receive (pymux, cli, variables) as input. + Commands will receive (pymux, variables) as input. Commands can raise CommandException. """ # Validate options. @@ -110,7 +109,7 @@ def cmd(name, options=''): pass def decorator(func): - def command_wrapper(pymux, cli, arguments): + def command_wrapper(pymux, arguments): # Hack to make the 'bind-key' option work. # (bind-key expects a variable number of arguments.) if name == 'bind-key' and '--' not in arguments: @@ -146,7 +145,7 @@ def command_wrapper(pymux, cli, arguments): raise CommandException('Usage: %s %s' % (name, options)) # Call handler. - func(pymux, cli, received_options) + func(pymux, received_options) # Invalidate all clients, not just the current CLI. pymux.invalidate() @@ -173,19 +172,19 @@ def __init__(self, message): @cmd('break-pane', options='[-d]') -def break_pane(pymux, cli, variables): +def break_pane(pymux, variables): dont_focus_window = variables['-d'] - pymux.arrangement.break_pane(cli, set_active=not dont_focus_window) + pymux.arrangement.break_pane(set_active=not dont_focus_window) pymux.invalidate() @cmd('select-pane', options='(-L|-R|-U|-D|-t )') -def select_pane(pymux, cli, variables): +def select_pane(pymux, variables): if variables['-t']: pane_id = variables[''] - w = pymux.arrangement.get_active_window(cli) + w = pymux.arrangement.get_active_window() if pane_id == ':.+': w.focus_next() @@ -205,11 +204,11 @@ def select_pane(pymux, cli, variables): if variables['-D']: h = focus_down if variables['-R']: h = focus_right - h(pymux, cli) + h(pymux) @cmd('select-window', options='(-t )') -def select_window(pymux, cli, variables): +def select_window(pymux, variables): """ Select a window. E.g: select-window -t :3 """ @@ -226,7 +225,7 @@ def invalid_window(): else: w = pymux.arrangement.get_window_by_index(number) if w: - pymux.arrangement.set_active_window(cli, w) + pymux.arrangement.set_active_window(w) else: invalid_window() else: @@ -234,7 +233,7 @@ def invalid_window(): @cmd('move-window', options='(-t )') -def move_window(pymux, cli, variables): +def move_window(pymux, variables): """ Move window to a new index. """ @@ -249,54 +248,54 @@ def move_window(pymux, cli, variables): raise CommandException("Can't move window: index in use.") # Save index. - w = pymux.arrangement.get_active_window(cli) + w = pymux.arrangement.get_active_window() pymux.arrangement.move_window(w, new_index) @cmd('rotate-window', options='[-D|-U]') -def rotate_window(pymux, cli, variables): +def rotate_window(pymux, variables): if variables['-D']: - pymux.arrangement.rotate_window(cli, count=-1) + pymux.arrangement.rotate_window(count=-1) else: - pymux.arrangement.rotate_window(cli) + pymux.arrangement.rotate_window() @cmd('swap-pane', options='(-D|-U)') -def swap_pane(pymux, cli, variables): - pymux.arrangement.get_active_window(cli).rotate(with_pane_after_only=variables['-U']) +def swap_pane(pymux, variables): + pymux.arrangement.get_active_window().rotate(with_pane_after_only=variables['-U']) @cmd('kill-pane') -def kill_pane(pymux, cli, variables): - pane = pymux.arrangement.get_active_pane(cli) +def kill_pane(pymux, variables): + pane = pymux.arrangement.get_active_pane() pymux.kill_pane(pane) @cmd('kill-window') -def kill_window(pymux, cli, variables): +def kill_window(pymux, variables): " Kill all panes in the current window. " - for pane in pymux.arrangement.get_active_window(cli).panes: + for pane in pymux.arrangement.get_active_window().panes: pymux.kill_pane(pane) @cmd('suspend-client') -def suspend_client(pymux, cli, variables): - connection = pymux.get_connection_for_cli(cli) +def suspend_client(pymux, variables): + connection = pymux.get_connection() if connection: connection.suspend_client_to_background() @cmd('clock-mode') -def clock_mode(pymux, cli, variables): - pane = pymux.arrangement.get_active_pane(cli) +def clock_mode(pymux, variables): + pane = pymux.arrangement.get_active_pane() if pane: pane.clock_mode = not pane.clock_mode @cmd('last-pane') -def last_pane(pymux, cli, variables): - w = pymux.arrangement.get_active_window(cli) +def last_pane(pymux, variables): + w = pymux.arrangement.get_active_window() prev_active_pane = w.previous_active_pane if prev_active_pane: @@ -304,79 +303,79 @@ def last_pane(pymux, cli, variables): @cmd('next-layout') -def next_layout(pymux, cli, variables): +def next_layout(pymux, variables): " Select next layout. " - pane = pymux.arrangement.get_active_window(cli) + pane = pymux.arrangement.get_active_window() if pane: pane.select_next_layout() @cmd('previous-layout') -def previous_layout(pymux, cli, variables): +def previous_layout(pymux, variables): " Select previous layout. " - pane = pymux.arrangement.get_active_window(cli) + pane = pymux.arrangement.get_active_window() if pane: pane.select_previous_layout() @cmd('new-window', options='[(-n )] [(-c )] []') -def new_window(pymux, cli, variables): +def new_window(pymux, variables): executable = variables[''] start_directory = variables[''] name = variables[''] - pymux.create_window(cli, executable, start_directory=start_directory, name=name) + pymux.create_window(executable, start_directory=start_directory, name=name) @cmd('next-window') -def next_window(pymux, cli, variables): +def next_window(pymux, variables): " Focus the next window. " - pymux.arrangement.focus_next_window(cli) + pymux.arrangement.focus_next_window() @cmd('last-window') -def _(pymux, cli, variables): +def _(pymux, variables): " Go to previous active window. " - w = pymux.arrangement.get_previous_active_window(cli) + w = pymux.arrangement.get_previous_active_window() if w: - pymux.arrangement.set_active_window(cli, w) + pymux.arrangement.set_active_window(w) @cmd('previous-window') -def previous_window(pymux, cli, variables): +def previous_window(pymux, variables): " Focus the previous window. " - pymux.arrangement.focus_previous_window(cli) + pymux.arrangement.focus_previous_window() @cmd('select-layout', options='') -def select_layout(pymux, cli, variables): +def select_layout(pymux, variables): layout_type = variables[''] if layout_type in LayoutTypes._ALL: - pymux.arrangement.get_active_window(cli).select_layout(layout_type) + pymux.arrangement.get_active_window().select_layout(layout_type) else: raise CommandException('Invalid layout type.') @cmd('rename-window', options='') -def rename_window(pymux, cli, variables): +def rename_window(pymux, variables): """ Rename the active window. """ - pymux.arrangement.get_active_window(cli).chosen_name = variables[''] + pymux.arrangement.get_active_window().chosen_name = variables[''] @cmd('rename-pane', options='') -def rename_pane(pymux, cli, variables): +def rename_pane(pymux, variables): """ Rename the active pane. """ - pymux.arrangement.get_active_pane(cli).chosen_name = variables[''] + pymux.arrangement.get_active_pane().chosen_name = variables[''] @cmd('rename-session', options='') -def rename_session(pymux, cli, variables): +def rename_session(pymux, variables): """ Rename this session. """ @@ -384,7 +383,7 @@ def rename_session(pymux, cli, variables): @cmd('split-window', options='[-v|-h] [(-c )] []') -def split_window(pymux, cli, variables): +def split_window(pymux, variables): """ Split horizontally or vertically. """ @@ -392,12 +391,12 @@ def split_window(pymux, cli, variables): start_directory = variables[''] # The tmux definition of horizontal is the opposite of prompt_toolkit. - pymux.add_process(cli, executable, vsplit=variables['-h'], + pymux.add_process(executable, vsplit=variables['-h'], start_directory=start_directory) @cmd('resize-pane', options="[(-L )] [(-U )] [(-D )] [(-R )] [-Z]") -def resize_pane(pymux, cli, variables): +def resize_pane(pymux, variables): """ Resize/zoom the active pane. """ @@ -409,7 +408,7 @@ def resize_pane(pymux, cli, variables): except ValueError: raise CommandException('Expecting an integer.') - w = pymux.arrangement.get_active_window(cli) + w = pymux.arrangement.get_active_window() if w: w.change_size_for_active_pane(up=up, right=right, down=down, left=left) @@ -420,27 +419,27 @@ def resize_pane(pymux, cli, variables): @cmd('detach-client') -def detach_client(pymux, cli, variables): +def detach_client(pymux, variables): """ Detach client. """ - pymux.detach_client(cli) + pymux.detach_client(get_app()) @cmd('confirm-before', options='[(-p )] ') -def confirm_before(pymux, cli, variables): - client_state = pymux.get_client_state(cli) +def confirm_before(pymux, variables): + client_state = pymux.get_client_state() client_state.confirm_text = variables[''] or '' client_state.confirm_command = variables[''] @cmd('command-prompt', options='[(-p )] [(-I )] []') -def command_prompt(pymux, cli, variables): +def command_prompt(pymux, variables): """ Enter command prompt. """ - client_state = pymux.get_client_state(cli) + client_state = pymux.get_client_state() if variables['']: # When a 'command' has been given. @@ -448,25 +447,27 @@ def command_prompt(pymux, cli, variables): client_state.prompt_command = variables[''] client_state.prompt_mode = True - cli.buffers[PROMPT].reset(Document( - format_pymux_string(pymux, cli, variables[''] or ''))) + client_state.prompt_buffer.reset(Document( + format_pymux_string(pymux, variables[''] or ''))) + + get_app().layout.focus(client_state.prompt_buffer) else: # Show the ':' prompt. client_state.prompt_text = '' client_state.prompt_command = '' - client_state.command_mode = True + get_app().layout.focus(client_state.command_buffer) # Go to insert mode. - client_state.vi_state.input_mode = InputMode.INSERT + get_app().vi_state.input_mode = InputMode.INSERT @cmd('send-prefix') -def send_prefix(pymux, cli, variables): +def send_prefix(pymux, variables): """ Send prefix to active pane. """ - process = pymux.arrangement.get_active_pane(cli).process + process = pymux.arrangement.get_active_pane().process for k in pymux.key_bindings_manager.prefix: vt100_data = prompt_toolkit_key_to_vt100_key(k) @@ -474,7 +475,7 @@ def send_prefix(pymux, cli, variables): @cmd('bind-key', options='[-n] [--] [...]') -def bind_key(pymux, cli, variables): +def bind_key(pymux, variables): """ Bind a key sequence. -n: Not necessary to use the prefix. @@ -492,7 +493,7 @@ def bind_key(pymux, cli, variables): @cmd('unbind-key', options='[-n] ') -def unbind_key(pymux, cli, variables): +def unbind_key(pymux, variables): """ Remove key binding. """ @@ -504,11 +505,11 @@ def unbind_key(pymux, cli, variables): @cmd('send-keys', options='...') -def send_keys(pymux, cli, variables): +def send_keys(pymux, variables): """ Send key strokes to the active process. """ - pane = pymux.arrangement.get_active_pane(cli) + pane = pymux.arrangement.get_active_pane() if pane.display_scroll_buffer: raise CommandException('Cannot send keys. Pane is in copy mode.') @@ -525,28 +526,29 @@ def send_keys(pymux, cli, variables): pane.process.write_key(k) -@cmd('copy-mode') -def copy_mode(pymux, cli, variables): +@cmd('copy-mode', options='[-u]') +def copy_mode(pymux, variables): """ Enter copy mode. """ - pane = pymux.arrangement.get_active_pane(cli) - pane.enter_copy_mode() + go_up = variables['-u'] # Go in copy mode and page-up directly. + # TODO: handle '-u' - cli.buffers[SEARCH_BUFFER].reset() + pane = pymux.arrangement.get_active_pane() + pane.enter_copy_mode() @cmd('paste-buffer') -def paste_buffer(pymux, cli, variables): +def paste_buffer(pymux, variables): """ Paste clipboard content into buffer. """ - pane = pymux.arrangement.get_active_pane(cli) - pane.process.write_input(cli.clipboard.get_data().text, paste=True) + pane = pymux.arrangement.get_active_pane() + pane.process.write_input(get_app().clipboard.get_data().text, paste=True) @cmd('source-file', options='') -def source_file(pymux, cli, variables): +def source_file(pymux, variables): """ Source configuration file. """ @@ -555,13 +557,13 @@ def source_file(pymux, cli, variables): with open(filename, 'rb') as f: for line in f: line = line.decode('utf-8') - handle_command(pymux, cli, line) + handle_command(pymux, line) except IOError as e: raise CommandException('IOError: %s' % (e, )) @cmd('set-option', options='