├── .gitignore
├── CHANGELOG
├── LICENSE
├── README.rst
├── examples
├── example-config.conf
└── true_color_test.py
├── images
├── copy-mode.png
├── menu-true-color.png
├── multiple-clients.png
└── pymux.png
├── pymux
├── __init__.py
├── __main__.py
├── arrangement.py
├── client
│ ├── __init__.py
│ ├── base.py
│ ├── defaults.py
│ ├── posix.py
│ └── windows.py
├── commands
│ ├── __init__.py
│ ├── aliases.py
│ ├── commands.py
│ ├── completer.py
│ └── utils.py
├── entry_points
│ ├── __init__.py
│ └── run_pymux.py
├── enums.py
├── filters.py
├── format.py
├── key_bindings.py
├── key_mappings.py
├── layout.py
├── log.py
├── main.py
├── options.py
├── pipes
│ ├── __init__.py
│ ├── base.py
│ ├── posix.py
│ ├── win32.py
│ ├── win32_client.py
│ └── win32_server.py
├── rc.py
├── server.py
├── style.py
└── utils.py
└── setup.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 |
5 | # C extensions
6 | *.so
7 |
8 | # Distribution / packaging
9 | .Python
10 | env/
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | lib/
17 | lib64/
18 | parts/
19 | sdist/
20 | var/
21 | *.egg-info/
22 | .installed.cfg
23 | *.egg
24 |
25 | # PyInstaller
26 | # Usually these files are written by a python script from a template
27 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
28 | *.manifest
29 | *.spec
30 |
31 | # Installer logs
32 | pip-log.txt
33 | pip-delete-this-directory.txt
34 |
35 | # Unit test / coverage reports
36 | htmlcov/
37 | .tox/
38 | .coverage
39 | .cache
40 | nosetests.xml
41 | coverage.xml
42 |
43 | # Translations
44 | *.mo
45 | *.pot
46 |
47 | # Django stuff:
48 | *.log
49 |
50 | # Sphinx documentation
51 | docs/_build/
52 |
53 | # PyBuilder
54 | target/
55 |
--------------------------------------------------------------------------------
/CHANGELOG:
--------------------------------------------------------------------------------
1 | CHANGELOG
2 | =========
3 |
4 | 0.14: 2017-07-27
5 | ----------------
6 |
7 | Fixes:
8 | - Fixed bug in remove_reader (ValueError).
9 | - Pin Pyte requirements belowe 0.6.0.
10 |
11 |
12 | 0.13: 2016-10-16
13 | ----------------
14 |
15 | New features:
16 | - Added status-interval option.
17 | - Support for ANSI colors only.
18 | * Added --ansicolor option.
19 | * Check PROMPT_TOOLKIT_ANSI_COLORS_ONLY environment variable.
20 | - Added pane-status option for hiding the pane status bar.
21 | (Disabled by default.)
22 | - Expose shift+arrow keys for key bindings.
23 |
24 | Performance improvements:
25 | - Only do datetime formatting if the string actually contains a '%'
26 | character.
27 |
28 | Fixes:
29 | - Catch OSError in os.tcgetpgrp.
30 | - Clean up sockets, also in case of a crash.
31 | - Fix in process.py: don't call remove_reader when master is None.
32 |
33 |
34 | 0.12: 2016-08-03
35 | ----------------
36 |
37 | Fixes:
38 | - Prompt_toolkit 1.0.4 compatibilty.
39 | - Python 2.6 compatibility.
40 |
41 |
42 | 0.11: 2016-06-27
43 | ----------------
44 |
45 | Fixes:
46 | - Fix for OS X El Capitan: LoadLibrary('libc.dylib') failed.
47 | - Compatibility with the latest prompt_toolkit.
48 |
49 |
50 | 0.10: 2016-05-05
51 | ----------------
52 |
53 | Upgrade to prompt_toolkit 1.0.0
54 |
55 | New features:
56 | - Added 'C-b PPage' key binding (like tmux).
57 | - Many performance improvements in the vt100 parser.
58 |
59 | Improvements/fixes:
60 | - Don't crash when decoding utf-8 input fails. (Sometimes it happens when using
61 | the mouse in lxterminal.)
62 | - Cleanup CLI object when the client was detached. (The server would become
63 | very slow if the CLI was not removed for a couple of times.)
64 | - Replace errors when decoding utf-8 input.
65 | - Fixes regarding multiwidth characters.
66 | - Bugfix: Don't use 'del' on a defaultdict, but use pop(..., None) instead, in
67 | order to avoid key errors.
68 | - Handle decomposed unicode characters correctly.
69 | - Bugfix regarding the handling of 'clear'.
70 | - Fixes a bug where the cursor stays at the top.
71 | - Fix: The socket in the pymux client should be blocking.
72 |
73 |
74 | 0.9: 2016-03-14
75 | ---------------
76 |
77 | Upgrade to prompt_toolkit 0.60
78 |
79 |
80 | 0.8: 2016-03-06
81 | ---------------
82 |
83 | Upgrade to prompt_toolkit 0.59
84 |
85 |
86 | 0.7: 2016-01-16
87 | ---------------
88 |
89 | Fixes:
90 | - Fixed FreeBSD support.
91 | - Compatibility with the latest Pyte version.
92 | - Handle 'No such process' in os.kill.
93 |
94 |
95 | 0.6: 2016-01-11
96 | ---------------
97 |
98 | Fixes:
99 | - Fix module import of pyte==0.5.1
100 | - Use gettempdir() for sockets.
101 | - Disable bracketed paste when leaving client.
102 | - Keep dimensions when closing a pane.
103 |
104 | New features:
105 | - Display the process name on Mac OS X.
106 | - Exit scroll buffer when pressing enter.
107 | - Added synchronize-panes window option.
108 |
109 |
110 | 0.5: 2016-01-05
111 | ----------------
112 |
113 | Fixes:
114 | - Handle KeyError in screen.insert_lines.
115 |
116 |
117 | 0.4: 2016-01-04
118 | ----------------
119 |
120 | Fixes:
121 | - After closing a pane, go to the previous pane.
122 | - Write crash reports to a secure temp file.
123 | - Added 'ls' as alias for list-sessions.
124 |
125 | Better performance:
126 | - Using a coroutine for the vt100 parser. (Much faster.)
127 | - Give priority to panes that have the focus.
128 | - Never postpone the rendering in case of high CPU. (Fix in prompt_toolkit.)
129 |
130 |
131 | 0.3: 2016-01-03
132 | ----------------
133 |
134 | New features:
135 | - Take $SHELL into account.
136 |
137 | Fixes:
138 | - Python 2 encoding bug fixed.
139 |
140 |
141 | 0.2: 2016-01-03
142 | ----------------
143 |
144 | First published version of Pymux, using prompt_toolkit.
145 |
146 |
147 | 0.1: 2014-02-19
148 | ---------------
149 |
150 | Initial experimental version of Pymux, written using asyncio. (This one is
151 | discontinued in favour of the new version, that uses prompt_toolkit.)
152 | still available here: https://github.com/jonathanslenders/old-pymux
153 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015, Jonathan Slenders
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification,
5 | are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | * Redistributions in binary form must reproduce the above copyright notice, this
11 | list of conditions and the following disclaimer in the documentation and/or
12 | other materials provided with the distribution.
13 |
14 | * Neither the name of the {organization} nor the names of its
15 | contributors may be used to endorse or promote products derived from
16 | this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Pymux
2 | =====
3 |
4 | WARNING: This project requires maintenance. The current master branch requires
5 | an old version of both prompt_toolkit and ptterm. There is a prompt-toolkit-3.0
6 | branch here that is compatible with the latest prompt_toolkit and the latest
7 | commit of the master branch of ptterm, but for that branch, only `pymux
8 | standalone` is working at the moment.
9 |
10 |
11 | *A terminal multiplexer (like tmux) in Python*
12 |
13 | ::
14 |
15 | pip install pymux
16 |
17 | .. image :: https://raw.githubusercontent.com/jonathanslenders/pymux/master/images/pymux.png
18 |
19 |
20 | Issues, questions, wishes, comments, feedback, remarks? Please create a GitHub
21 | issue, I appreciate it.
22 |
23 |
24 | Installation
25 | ------------
26 |
27 | Simply install ``pymux`` using pip:
28 |
29 | ::
30 |
31 | pip install pymux
32 |
33 | Start it by typing ``pymux``.
34 |
35 |
36 | What does it do?
37 | ----------------
38 |
39 | A terminal multiplexer makes it possible to run multiple applications in the
40 | same terminal. It does this by emulating a vt100 terminal for each application.
41 | There are serveral programs doing this. The most famous are `GNU Screen
42 | `_ and `tmux `_.
43 |
44 | Pymux is written entirely in Python. It doesn't need any C extension. It runs
45 | on all Python versions from 2.6 until 3.5. It should work on OS X and Linux.
46 |
47 |
48 | Compared to tmux
49 | ----------------
50 |
51 | To some extent, pymux is a clone of tmux. This means that all the default
52 | shortcuts are the same; the commands are the same or very similar, and even a
53 | simple configuration file could be the same. (There are some small
54 | incompatibilities.) However, we definitely don't intend to create a fully
55 | compatible clone. Right now, only a subset of the command options that tmux
56 | provides are supported.
57 |
58 | Pymux implements a few improvements over tmux:
59 |
60 | - There is a completion menu for the command line. (At the bottom of the screen.)
61 | - The command line has `fish-style `_ suggestions.
62 | - Both Emacs and Vi key bindings for the command line and copy buffer are well
63 | developed, thanks to all the effort we have put earlier in `prompt_toolkit
64 | `_.
65 | - Search in the copy buffer is highlighted while searching.
66 | - Every pane has its own titlebar.
67 | - When several clients are attached to the same session, each client can watch
68 | a different window. When clients are watching different windows, every client
69 | uses the full terminal size.
70 | - Support for 24bit true color. (Disabled by default: not all terminals support
71 | it. Use the ``--truecolor`` option at startup or during attach in order to
72 | enable it.)
73 | - Support for unicode input and output. Pymux correctly understands utf-8
74 | encoded double width characters. (Also for the titlebars.)
75 |
76 | About the performance:
77 |
78 | - Tmux is written in C, which is obviously faster than Python. This is
79 | noticeable when applications generate a lot of output. Where tmux is able to
80 | give fast real-time output for, for instance ``find /`` or ``yes``, pymux
81 | will process the output slightly slower, and in this case render the output
82 | only a few times per second to the terminal. Usually, this should not be an
83 | issue. If it is, `Pypy `_ should provide a significant
84 | speedup.
85 |
86 | The big advantage of using Python and `prompt_toolkit
87 | `_ is that the
88 | implementation of new features becomes very easy.
89 |
90 |
91 | More screenshots
92 | ----------------
93 |
94 | 24 bit color support and the autocompletion menu:
95 |
96 | .. image :: https://raw.githubusercontent.com/jonathanslenders/pymux/master/images/menu-true-color.png
97 |
98 | What happens if another client with a smaller screen size attaches:
99 |
100 | .. image :: https://raw.githubusercontent.com/jonathanslenders/pymux/master/images/multiple-clients.png
101 |
102 | When a pane enters copy mode, search results are highlighted:
103 |
104 | .. image :: https://raw.githubusercontent.com/jonathanslenders/pymux/master/images/copy-mode.png
105 |
106 |
107 | Why create a tmux clone?
108 | ------------------------
109 |
110 | For several reasons. Having a terminal multiplexer in Python makes it easy to
111 | experiment and implement new features. While C is a good language, it's not as
112 | easy to develop as Python.
113 |
114 | Just like `pyvim `_ (A ``Vi`` clone
115 | in Python.), it started as another experiment. A project to challenge the
116 | design of prompt_toolkit. At this point, however, pymux should be stable and
117 | usable for daily work.
118 |
119 | The development resulted in many improvements in prompt_toolkit, especially
120 | performance improvements, but also some functionality improvements.
121 |
122 | Further, the development is especially interesting, because it touches so many
123 | different areas that are unknown to most Python developers. It also proves that
124 | Python is a good tool to create terminal applications.
125 |
126 |
127 | The roadmap
128 | -----------
129 |
130 | There is no official roadmap, the code is mostly written for the fun and of
131 | course, time is limited, but I use pymux professionally and I'm eager to
132 | implement new ideas.
133 |
134 | Some ideas:
135 |
136 | - Support for color schemes.
137 | - Support for extensions written in Python.
138 | - Better support for scripting. (Right now, it's already possible to run pymux
139 | commands from inside the shell of a pane. E.g. ``pymux split-window``.
140 | However, status codes and feedback aren't transferred yet.)
141 | - Improved mouse support. (Reporting of mouse movement.)
142 | - Parts of pymux could become a library, so that any prompt_toolkit application
143 | can embed a vt100 terminal. (Imagine a terminal emulator embedded in `pyvim
144 | `_.)
145 | - Maybe some cool widgets to traverse the windows and panes.
146 | - Better autocompletion.
147 |
148 |
149 | Configuring
150 | -----------
151 |
152 | Create a file ``~/.pymux.conf``, and populate it with commands, like you can
153 | enter at the command line. There is an `example config
154 | `_
155 | in the examples directory.
156 |
157 |
158 | What if it crashes?
159 | -------------------
160 |
161 | If for some reason pymux crashes, it will attempt to write a stack trace to a
162 | file with a name like ``/tmp/pymux.crash-*``. It is possible that the user
163 | interface freezes. Please create a GitHub issue with this stack trace.
164 |
165 |
166 | Special thanks
167 | --------------
168 |
169 | - `Pyte `_, for providing a working vt100
170 | parser. (This one is extended in order to support some xterm extensions.)
171 | - `docopt `_, for parsing the command line arguments.
172 | - `prompt_toolkit
173 | `_, for the UI
174 | toolkit.
175 | - `wcwidth `_: for better unicode support
176 | (support of double width characters).
177 | - `tmux `_, for the inspiration.
178 |
--------------------------------------------------------------------------------
/examples/example-config.conf:
--------------------------------------------------------------------------------
1 | # Example pymux configuration.
2 | # Copy to ~/.pymux.conf and modify.
3 |
4 |
5 | # Use Control-A as a prefix.
6 | set-option prefix C-a
7 | unbind C-b
8 | bind C-a send-prefix
9 |
10 |
11 | # Rename panes with ':
12 | bind-key "'" command-prompt -p '(rename-pane)' 'rename-pane "%%"'
13 |
14 |
15 | # Open 'htop' with t
16 | bind-key t split-window -h htop
17 |
18 |
19 | # Use '|' and '-' for splitting panes.
20 | # (The double dash after '-' is required due to a bug in docopt.)
21 | bind-key "|" split-window -h
22 | bind-key "-" -- split-window -v
23 |
24 |
25 | # Use Vi key bindings instead of emacs. (For both the status bar and copy
26 | # mode.)
27 | set-option mode-keys vi
28 | set-option status-keys vi
29 |
30 |
31 | # Display the hostname on the left side of the status bar.
32 | set-option status-left '[#h:#S] '
33 |
34 |
35 | # Start numbering windows from 1.
36 | set-option base-index 1
37 |
--------------------------------------------------------------------------------
/examples/true_color_test.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """
3 | Run this script inside 'pymux' in order to discover whether or not in supports
4 | true 24bit color. It should display a rectangle with both red and green values
5 | changing between 0 and 80.
6 | """
7 | from __future__ import unicode_literals, print_function
8 |
9 | i = 0
10 | for r in range(0, 80):
11 | for g in range(0, 80):
12 | b = 1
13 | print('\x1b[0;48;2;%s;%s;%sm ' % (r, g, b), end='')
14 | if i == 1000:
15 | break
16 |
17 | print('\x1b[0m \n', end='')
18 | print('\x1b[0m\r\n')
19 |
--------------------------------------------------------------------------------
/images/copy-mode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prompt-toolkit/pymux/163eeb31e516b0f6356af46082e8e7aea708386b/images/copy-mode.png
--------------------------------------------------------------------------------
/images/menu-true-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prompt-toolkit/pymux/163eeb31e516b0f6356af46082e8e7aea708386b/images/menu-true-color.png
--------------------------------------------------------------------------------
/images/multiple-clients.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prompt-toolkit/pymux/163eeb31e516b0f6356af46082e8e7aea708386b/images/multiple-clients.png
--------------------------------------------------------------------------------
/images/pymux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prompt-toolkit/pymux/163eeb31e516b0f6356af46082e8e7aea708386b/images/pymux.png
--------------------------------------------------------------------------------
/pymux/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
--------------------------------------------------------------------------------
/pymux/__main__.py:
--------------------------------------------------------------------------------
1 | """
2 | Make sure `python -m pymux` works.
3 | """
4 | from __future__ import unicode_literals
5 | from .entry_points.run_pymux import run
6 |
7 | if __name__ == '__main__':
8 | run()
9 |
--------------------------------------------------------------------------------
/pymux/arrangement.py:
--------------------------------------------------------------------------------
1 | """
2 | Arrangement of panes.
3 |
4 | Don't confuse with the prompt_toolkit VSplit/HSplit classes. This is a higher
5 | level abstraction of the Pymux window layout.
6 |
7 | An arrangement consists of a list of windows. And a window has a list of panes,
8 | arranged by ordering them in HSplit/VSplit instances.
9 | """
10 | from __future__ import unicode_literals
11 |
12 | from ptterm import Terminal
13 | from prompt_toolkit.application.current import get_app, set_app
14 | from prompt_toolkit.buffer import Buffer
15 |
16 | import math
17 | import os
18 | import weakref
19 | import six
20 |
21 | __all__ = (
22 | 'LayoutTypes',
23 | 'Pane',
24 | 'HSplit',
25 | 'VSplit',
26 | 'Window',
27 | 'Arrangement',
28 | )
29 |
30 |
31 | class LayoutTypes:
32 | # The values are in lowercase with dashes, because that is what users can
33 | # use at the command line.
34 | EVEN_HORIZONTAL = 'even-horizontal'
35 | EVEN_VERTICAL = 'even-vertical'
36 | MAIN_HORIZONTAL = 'main-horizontal'
37 | MAIN_VERTICAL = 'main-vertical'
38 | TILED = 'tiled'
39 |
40 | _ALL = [EVEN_HORIZONTAL, EVEN_VERTICAL, MAIN_HORIZONTAL, MAIN_VERTICAL, TILED]
41 |
42 |
43 | class Pane(object):
44 | """
45 | One pane, containing one process and a search buffer for going into copy
46 | mode or displaying the help.
47 | """
48 | _pane_counter = 1000 # Start at 1000, to be sure to not confuse this with pane indexes.
49 |
50 | def __init__(self, terminal=None):
51 | assert isinstance(terminal, Terminal)
52 |
53 | self.terminal = terminal
54 | self.chosen_name = None
55 |
56 | # Displayed the clock instead of this pane content.
57 | self.clock_mode = False
58 |
59 | # Give unique ID.
60 | Pane._pane_counter += 1
61 | self.pane_id = Pane._pane_counter
62 |
63 | # Prompt_toolkit buffer, for displaying scrollable text.
64 | # (In copy mode, or help mode.)
65 | # Note: Because the scroll_buffer can only contain text, we also use the
66 | # get_tokens_for_line, that returns the token list with color
67 | # information for each line.
68 | self.scroll_buffer = Buffer(read_only=True)
69 | self.copy_get_tokens_for_line = lambda lineno: []
70 | self.display_scroll_buffer = False
71 | self.scroll_buffer_title = ''
72 |
73 | @property
74 | def process(self):
75 | return self.terminal.process
76 |
77 | @property
78 | def name(self):
79 | """
80 | The name for the window as displayed in the title bar and status bar.
81 | """
82 | # Name, explicitely set for the pane.
83 | if self.chosen_name:
84 | return self.chosen_name
85 | else:
86 | # Name from the process running inside the pane.
87 | name = self.process.get_name()
88 | if name:
89 | return os.path.basename(name)
90 |
91 | return ''
92 |
93 | def enter_copy_mode(self):
94 | """
95 | Suspend the process, and copy the screen content to the `scroll_buffer`.
96 | That way the user can search through the history and copy/paste.
97 | """
98 | self.terminal.enter_copy_mode()
99 |
100 | def focus(self):
101 | """
102 | Focus this pane.
103 | """
104 | get_app().layout.focus(self.terminal)
105 |
106 |
107 | class _WeightsDictionary(weakref.WeakKeyDictionary):
108 | """
109 | Dictionary for the weights: weak keys, but defaults to 1.
110 |
111 | (Weights are used to represent the proportion of pane sizes in
112 | HSplit/VSplit lists.)
113 |
114 | This dictionary maps the child (another HSplit/VSplit or Pane), to the
115 | size. (Integer.)
116 | """
117 | def __getitem__(self, key):
118 | try:
119 | # (Don't use 'super' here. This is a classobj in Python2.)
120 | return weakref.WeakKeyDictionary.__getitem__(self, key)
121 | except KeyError:
122 | return 1
123 |
124 |
125 | class _Split(list):
126 | """
127 | Base class for horizontal and vertical splits. (This is a higher level
128 | split than prompt_toolkit.layout.HSplit.)
129 | """
130 | def __init__(self, *a, **kw):
131 | list.__init__(self, *a, **kw)
132 |
133 | # Mapping children to its weight.
134 | self.weights = _WeightsDictionary()
135 |
136 | def __hash__(self):
137 | # Required in order to add HSplit/VSplit to the weights dict. "
138 | return id(self)
139 |
140 | def __repr__(self):
141 | return '%s(%s)' % (self.__class__.__name__, list.__repr__(self))
142 |
143 |
144 | class HSplit(_Split):
145 | """ Horizontal split. """
146 |
147 |
148 | class VSplit(_Split):
149 | """ Horizontal split. """
150 |
151 |
152 | class Window(object):
153 | """
154 | Pymux window.
155 | """
156 | _window_counter = 1000 # Start here, to avoid confusion with window index.
157 |
158 | def __init__(self, index=0):
159 | self.index = index
160 | self.root = HSplit()
161 | self._active_pane = None
162 | self._prev_active_pane = None
163 | self.chosen_name = None
164 | self.previous_selected_layout = None
165 |
166 | #: When true, the current pane is zoomed in.
167 | self.zoom = False
168 |
169 | #: When True, send input to all panes simultaniously.
170 | self.synchronize_panes = False
171 |
172 | # Give unique ID.
173 | Window._window_counter += 1
174 | self.window_id = Window._window_counter
175 |
176 | def invalidation_hash(self):
177 | """
178 | Return a hash (string) that can be used to determine when the layout
179 | has to be rebuild.
180 | """
181 | # if not self.root:
182 | # return ''
183 |
184 | def _hash_for_split(split):
185 | result = []
186 | for item in split:
187 | if isinstance(item, (VSplit, HSplit)):
188 | result.append(_hash_for_split(item))
189 | elif isinstance(item, Pane):
190 | result.append('p%s' % item.pane_id)
191 |
192 | if isinstance(split, HSplit):
193 | return 'HSplit(%s)' % (','.join(result))
194 | else:
195 | return 'VSplit(%s)' % (','.join(result))
196 |
197 | return '' % (
198 | self.window_id, self.zoom, _hash_for_split(self.root))
199 |
200 | @property
201 | def active_pane(self):
202 | """
203 | The current active :class:`.Pane`.
204 | """
205 | return self._active_pane
206 |
207 | @active_pane.setter
208 | def active_pane(self, value):
209 | assert isinstance(value, Pane)
210 |
211 | # Remember previous active pane.
212 | if self._active_pane:
213 | self._prev_active_pane = weakref.ref(self._active_pane)
214 |
215 | self.zoom = False
216 | self._active_pane = value
217 |
218 | @property
219 | def previous_active_pane(self):
220 | """
221 | The previous active :class:`.Pane` or `None` if unknown.
222 | """
223 | p = self._prev_active_pane and self._prev_active_pane()
224 |
225 | # Only return when this pane actually still exists in the current
226 | # window.
227 | if p and p in self.panes:
228 | return p
229 |
230 | @property
231 | def name(self):
232 | """
233 | The name for this window as it should be displayed in the status bar.
234 | """
235 | # Name, explicitely set for the window.
236 | if self.chosen_name:
237 | return self.chosen_name
238 | else:
239 | pane = self.active_pane
240 | if pane:
241 | return pane.name
242 |
243 | return ''
244 |
245 | def add_pane(self, pane, vsplit=False):
246 | """
247 | Add another pane to this Window.
248 | """
249 | assert isinstance(pane, Pane)
250 | assert isinstance(vsplit, bool)
251 |
252 | split_cls = VSplit if vsplit else HSplit
253 |
254 | if self.active_pane is None:
255 | self.root.append(pane)
256 | else:
257 | parent = self._get_parent(self.active_pane)
258 | same_direction = isinstance(parent, split_cls)
259 |
260 | index = parent.index(self.active_pane)
261 |
262 | if same_direction:
263 | parent.insert(index + 1, pane)
264 | else:
265 | new_split = split_cls([self.active_pane, pane])
266 | parent[index] = new_split
267 |
268 | # Give the newly created split the same weight as the original
269 | # pane that was at this position.
270 | parent.weights[new_split] = parent.weights[self.active_pane]
271 |
272 | self.active_pane = pane
273 | self.zoom = False
274 |
275 | def remove_pane(self, pane):
276 | """
277 | Remove pane from this Window.
278 | """
279 | assert isinstance(pane, Pane)
280 |
281 | if pane in self.panes:
282 | # When this pane was focused, switch to previous active or next in order.
283 | if pane == self.active_pane:
284 | if self.previous_active_pane:
285 | self.active_pane = self.previous_active_pane
286 | else:
287 | self.focus_next()
288 |
289 | # Remove from the parent. When the parent becomes empty, remove the
290 | # parent itself recursively.
291 | p = self._get_parent(pane)
292 | p.remove(pane)
293 |
294 | while len(p) == 0 and p != self.root:
295 | p2 = self._get_parent(p)
296 | p2.remove(p)
297 | p = p2
298 |
299 | # When the parent has only one item left, collapse into its parent.
300 | while len(p) == 1 and p != self.root:
301 | p2 = self._get_parent(p)
302 | p2.weights[p[0]] = p2.weights[p] # Keep dimensions.
303 | i = p2.index(p)
304 | p2[i] = p[0]
305 | p = p2
306 |
307 | @property
308 | def panes(self):
309 | " List with all panes from this Window. "
310 | result = []
311 |
312 | for s in self.splits:
313 | for item in s:
314 | if isinstance(item, Pane):
315 | result.append(item)
316 |
317 | return result
318 |
319 | @property
320 | def splits(self):
321 | " Return a list with all HSplit/VSplit instances. "
322 | result = []
323 |
324 | def collect(split):
325 | result.append(split)
326 |
327 | for item in split:
328 | if isinstance(item, (HSplit, VSplit)):
329 | collect(item)
330 |
331 | collect(self.root)
332 | return result
333 |
334 | def _get_parent(self, item):
335 | " The HSplit/VSplit that contains the active pane. "
336 | for s in self.splits:
337 | if item in s:
338 | return s
339 |
340 | @property
341 | def has_panes(self):
342 | " True when this window contains at least one pane. "
343 | return len(self.panes) > 0
344 |
345 | @property
346 | def active_process(self):
347 | " Return `Process` that should receive user input. "
348 | p = self.active_pane
349 |
350 | if p is not None:
351 | return p.process
352 |
353 | def focus_next(self, count=1):
354 | " Focus the next pane. "
355 | panes = self.panes
356 | if panes:
357 | self.active_pane = panes[(panes.index(self.active_pane) + count) % len(panes)]
358 | else:
359 | self.active_pane = None # No panes left.
360 |
361 | def focus_previous(self):
362 | " Focus the previous pane. "
363 | self.focus_next(count=-1)
364 |
365 | def rotate(self, count=1, with_pane_before_only=False, with_pane_after_only=False):
366 | """
367 | Rotate panes.
368 | When `with_pane_before_only` or `with_pane_after_only` is True, only rotate
369 | with the pane before/after the active pane.
370 | """
371 | # Create (split, index, pane, weight) tuples.
372 | items = []
373 | current_pane_index = None
374 |
375 | for s in self.splits:
376 | for index, item in enumerate(s):
377 | if isinstance(item, Pane):
378 | items.append((s, index, item, s.weights[item]))
379 | if item == self.active_pane:
380 | current_pane_index = len(items) - 1
381 |
382 | # Only before after? Reduce list of panes.
383 | if with_pane_before_only:
384 | items = items[current_pane_index - 1:current_pane_index + 1]
385 |
386 | elif with_pane_after_only:
387 | items = items[current_pane_index:current_pane_index + 2]
388 |
389 | # Rotate positions.
390 | for i, triple in enumerate(items):
391 | split, index, pane, weight = triple
392 |
393 | new_item = items[(i + count) % len(items)][2]
394 |
395 | split[index] = new_item
396 | split.weights[new_item] = weight
397 |
398 | def select_layout(self, layout_type):
399 | """
400 | Select one of the predefined layouts.
401 | """
402 | assert layout_type in LayoutTypes._ALL
403 |
404 | # When there is only one pane, always choose EVEN_HORIZONTAL,
405 | # Otherwise, we create VSplit/HSplit instances with an empty list of
406 | # children.
407 | if len(self.panes) == 1:
408 | layout_type = LayoutTypes.EVEN_HORIZONTAL
409 |
410 | # even-horizontal.
411 | if layout_type == LayoutTypes.EVEN_HORIZONTAL:
412 | self.root = HSplit(self.panes)
413 |
414 | # even-vertical.
415 | elif layout_type == LayoutTypes.EVEN_VERTICAL:
416 | self.root = VSplit(self.panes)
417 |
418 | # main-horizontal.
419 | elif layout_type == LayoutTypes.MAIN_HORIZONTAL:
420 | self.root = HSplit([
421 | self.active_pane,
422 | VSplit([p for p in self.panes if p != self.active_pane])
423 | ])
424 |
425 | # main-vertical.
426 | elif layout_type == LayoutTypes.MAIN_VERTICAL:
427 | self.root = VSplit([
428 | self.active_pane,
429 | HSplit([p for p in self.panes if p != self.active_pane])
430 | ])
431 |
432 | # tiled.
433 | elif layout_type == LayoutTypes.TILED:
434 | panes = self.panes
435 | column_count = math.ceil(len(panes) ** .5)
436 |
437 | rows = HSplit()
438 | current_row = VSplit()
439 |
440 | for p in panes:
441 | current_row.append(p)
442 |
443 | if len(current_row) >= column_count:
444 | rows.append(current_row)
445 | current_row = VSplit()
446 | if current_row:
447 | rows.append(current_row)
448 |
449 | self.root = rows
450 |
451 | self.previous_selected_layout = layout_type
452 |
453 | def select_next_layout(self, count=1):
454 | """
455 | Select next layout. (Cycle through predefined layouts.)
456 | """
457 | # List of all layouts. (When we have just two panes, only toggle
458 | # between horizontal/vertical.)
459 | if len(self.panes) == 2:
460 | all_layouts = [LayoutTypes.EVEN_HORIZONTAL, LayoutTypes.EVEN_VERTICAL]
461 | else:
462 | all_layouts = LayoutTypes._ALL
463 |
464 | # Get index of current layout.
465 | layout = self.previous_selected_layout or LayoutTypes._ALL[-1]
466 | try:
467 | index = all_layouts.index(layout)
468 | except ValueError:
469 | index = 0
470 |
471 | # Switch to new layout.
472 | new_layout = all_layouts[(index + count) % len(all_layouts)]
473 | self.select_layout(new_layout)
474 |
475 | def select_previous_layout(self):
476 | self.select_next_layout(count=-1)
477 |
478 | def change_size_for_active_pane(self, up=0, right=0, down=0, left=0):
479 | """
480 | Increase the size of the current pane in any of the four directions.
481 | """
482 | child = self.active_pane
483 | self.change_size_for_pane(child, up=up, right=right, down=down, left=left)
484 |
485 | def change_size_for_pane(self, pane, up=0, right=0, down=0, left=0):
486 | """
487 | Increase the size of the current pane in any of the four directions.
488 | Positive values indicate an increase, negative values a decrease.
489 | """
490 | assert isinstance(pane, Pane)
491 |
492 | def find_split_and_child(split_cls, is_before):
493 | " Find the split for which we will have to update the weights. "
494 | child = pane
495 | split = self._get_parent(child)
496 |
497 | def found():
498 | return isinstance(split, split_cls) and (
499 | not is_before or split.index(child) > 0) and (
500 | is_before or split.index(child) < len(split) - 1)
501 |
502 | while split and not found():
503 | child = split
504 | split = self._get_parent(child)
505 |
506 | return split, child # split can be None!
507 |
508 | def handle_side(split_cls, is_before, amount, trying_other_side=False):
509 | " Increase weights on one side. (top/left/right/bottom). "
510 | if amount:
511 | split, child = find_split_and_child(split_cls, is_before)
512 |
513 | if split:
514 | # Find neighbour.
515 | neighbour_index = split.index(child) + (-1 if is_before else 1)
516 | neighbour_child = split[neighbour_index]
517 |
518 | # Increase/decrease weights.
519 | split.weights[child] += amount
520 | split.weights[neighbour_child] -= amount
521 |
522 | # Ensure that all weights are at least one.
523 | for k, value in split.weights.items():
524 | if value < 1:
525 | split.weights[k] = 1
526 |
527 | else:
528 | # When no split has been found where we can move in this
529 | # direction, try to move the other side instead using a
530 | # negative amount. This happens when we run "resize-pane -R 4"
531 | # inside the pane that is completely on the right. In that
532 | # case it's logical to move the left border to the right
533 | # instead.
534 | if not trying_other_side:
535 | handle_side(split_cls, not is_before, -amount,
536 | trying_other_side=True)
537 |
538 | handle_side(VSplit, True, left)
539 | handle_side(VSplit, False, right)
540 | handle_side(HSplit, True, up)
541 | handle_side(HSplit, False, down)
542 |
543 | def get_pane_index(self, pane):
544 | " Return the index of the given pane. ValueError if not found. "
545 | assert isinstance(pane, Pane)
546 | return self.panes.index(pane)
547 |
548 |
549 | class Arrangement(object):
550 | """
551 | Arrangement class for one Pymux session.
552 | This contains the list of windows and the layout of the panes for each
553 | window. All the clients share the same Arrangement instance, but they can
554 | have different windows active.
555 | """
556 | def __init__(self):
557 | self.windows = []
558 | self.base_index = 0
559 |
560 | self._active_window_for_cli = weakref.WeakKeyDictionary()
561 | self._prev_active_window_for_cli = weakref.WeakKeyDictionary()
562 |
563 | # The active window of the last CLI. Used as default when a new session
564 | # is attached.
565 | self._last_active_window = None
566 |
567 | def invalidation_hash(self):
568 | """
569 | When this changes, the layout needs to be rebuild.
570 | """
571 | if not self.windows:
572 | return ''
573 |
574 | w = self.get_active_window()
575 | return w.invalidation_hash()
576 |
577 | def get_active_window(self):
578 | """
579 | The current active :class:`.Window`.
580 | """
581 | app = get_app()
582 |
583 | try:
584 | return self._active_window_for_cli[app]
585 | except KeyError:
586 | self._active_window_for_cli[app] = self._last_active_window or self.windows[0]
587 | return self.windows[0]
588 |
589 | def set_active_window(self, window):
590 | assert isinstance(window, Window)
591 | app = get_app()
592 |
593 | previous = self.get_active_window()
594 | self._prev_active_window_for_cli[app] = previous
595 | self._active_window_for_cli[app] = window
596 | self._last_active_window = window
597 |
598 | def set_active_window_from_pane_id(self, pane_id):
599 | """
600 | Make the window with this pane ID the active Window.
601 | """
602 | assert isinstance(pane_id, int)
603 |
604 | for w in self.windows:
605 | for p in w.panes:
606 | if p.pane_id == pane_id:
607 | self.set_active_window(w)
608 |
609 | def get_previous_active_window(self):
610 | " The previous active Window or None if unknown. "
611 | app = get_app()
612 |
613 | try:
614 | return self._prev_active_window_for_cli[app]
615 | except KeyError:
616 | return None
617 |
618 | def get_window_by_index(self, index):
619 | " Return the Window with this index or None if not found. "
620 | for w in self.windows:
621 | if w.index == index:
622 | return w
623 |
624 | def create_window(self, pane, name=None, set_active=True):
625 | """
626 | Create a new window that contains just this pane.
627 |
628 | :param pane: The :class:`.Pane` instance to put in the new window.
629 | :param name: If given, name for the new window.
630 | :param set_active: When True, focus the new window.
631 | """
632 | assert isinstance(pane, Pane)
633 | assert name is None or isinstance(name, six.text_type)
634 |
635 | # Take the first available index.
636 | taken_indexes = [w.index for w in self.windows]
637 |
638 | index = self.base_index
639 | while index in taken_indexes:
640 | index += 1
641 |
642 | # Create new window and add it.
643 | w = Window(index)
644 | w.add_pane(pane)
645 | self.windows.append(w)
646 |
647 | # Sort windows by index.
648 | self.windows = sorted(self.windows, key=lambda w: w.index)
649 |
650 | app = get_app(return_none=True)
651 |
652 | if app is not None and set_active:
653 | self.set_active_window(w)
654 |
655 | if name is not None:
656 | w.chosen_name = name
657 |
658 | assert w.active_pane == pane
659 | assert w._get_parent(pane)
660 |
661 | def move_window(self, window, new_index):
662 | """
663 | Move window to a new index.
664 | """
665 | assert isinstance(window, Window)
666 | assert isinstance(new_index, int)
667 |
668 | window.index = new_index
669 |
670 | # Sort windows by index.
671 | self.windows = sorted(self.windows, key=lambda w: w.index)
672 |
673 | def get_active_pane(self):
674 | """
675 | The current :class:`.Pane` from the current window.
676 | """
677 | w = self.get_active_window()
678 | if w is not None:
679 | return w.active_pane
680 |
681 | def remove_pane(self, pane):
682 | """
683 | Remove a :class:`.Pane`. (Look in all windows.)
684 | """
685 | assert isinstance(pane, Pane)
686 |
687 | for w in self.windows:
688 | w.remove_pane(pane)
689 |
690 | # No panes left in this window?
691 | if not w.has_panes:
692 | # Focus next.
693 | for app, active_w in self._active_window_for_cli.items():
694 | if w == active_w:
695 | with set_app(app):
696 | self.focus_next_window()
697 |
698 | self.windows.remove(w)
699 |
700 | def focus_previous_window(self):
701 | w = self.get_active_window()
702 |
703 | self.set_active_window(self.windows[
704 | (self.windows.index(w) - 1) % len(self.windows)])
705 |
706 | def focus_next_window(self):
707 | w = self.get_active_window()
708 |
709 | self.set_active_window(self.windows[
710 | (self.windows.index(w) + 1) % len(self.windows)])
711 |
712 | def break_pane(self, set_active=True):
713 | """
714 | When the current window has multiple panes, remove the pane from this
715 | window and put it in a new window.
716 |
717 | :param set_active: When True, focus the new window.
718 | """
719 | w = self.get_active_window()
720 |
721 | if len(w.panes) > 1:
722 | pane = w.active_pane
723 | self.get_active_window().remove_pane(pane)
724 | self.create_window(pane, set_active=set_active)
725 |
726 | def rotate_window(self, count=1):
727 | " Rotate the panes in the active window. "
728 | w = self.get_active_window()
729 | w.rotate(count=count)
730 |
731 | @property
732 | def has_panes(self):
733 | " True when any of the windows has a :class:`.Pane`. "
734 | for w in self.windows:
735 | if w.has_panes:
736 | return True
737 | return False
738 |
--------------------------------------------------------------------------------
/pymux/client/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 | from .base import Client
3 | from .defaults import create_client, list_clients
4 |
--------------------------------------------------------------------------------
/pymux/client/base.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
3 | from prompt_toolkit.output import ColorDepth
4 | from abc import ABCMeta
5 | from six import with_metaclass
6 |
7 |
8 | __all__ = [
9 | 'Client',
10 | ]
11 |
12 |
13 | class Client(with_metaclass(ABCMeta, object)):
14 | def run_command(self, command, pane_id=None):
15 | """
16 | Ask the server to run this command.
17 | """
18 |
19 | def attach(self, detach_other_clients=False, color_depth=ColorDepth.DEPTH_8_BIT):
20 | """
21 | Attach client user interface.
22 | """
23 |
--------------------------------------------------------------------------------
/pymux/client/defaults.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 | from prompt_toolkit.utils import is_windows
3 | __all__ = [
4 | 'create_client',
5 | 'list_clients',
6 | ]
7 |
8 |
9 | def create_client(socket_name):
10 | if is_windows():
11 | from .windows import WindowsClient
12 | return WindowsClient(socket_name)
13 | else:
14 | from .posix import PosixClient
15 | return PosixClient(socket_name)
16 |
17 |
18 | def list_clients():
19 | if is_windows():
20 | from .windows import list_clients
21 | return list_clients()
22 | else:
23 | from .posix import list_clients
24 | return list_clients()
25 |
--------------------------------------------------------------------------------
/pymux/client/posix.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
3 | from prompt_toolkit.eventloop.select import select_fds
4 | from prompt_toolkit.input.posix_utils import PosixStdinReader
5 | from prompt_toolkit.input.vt100 import raw_mode, cooked_mode
6 | from prompt_toolkit.output.vt100 import _get_size, Vt100_Output
7 | from prompt_toolkit.output import ColorDepth
8 |
9 | from pymux.utils import nonblocking
10 |
11 | import getpass
12 | import glob
13 | import json
14 | import os
15 | import signal
16 | import socket
17 | import sys
18 | import tempfile
19 | from .base import Client
20 |
21 | INPUT_TIMEOUT = .5
22 |
23 | __all__ = (
24 | 'PosixClient',
25 | 'list_clients',
26 | )
27 |
28 |
29 | class PosixClient(Client):
30 | def __init__(self, socket_name):
31 | self.socket_name = socket_name
32 | self._mode_context_managers = []
33 |
34 | # Connect to socket.
35 | self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
36 | self.socket.connect(socket_name)
37 | self.socket.setblocking(1)
38 |
39 | # Input reader.
40 | # Some terminals, like lxterminal send non UTF-8 input sequences,
41 | # even when the input encoding is supposed to be UTF-8. This
42 | # happens in the case of mouse clicks in the right area of a wide
43 | # terminal. Apparently, these are some binary blobs in between the
44 | # UTF-8 input.)
45 | # We should not replace these, because this would break the
46 | # decoding otherwise. (Also don't pass errors='ignore', because
47 | # that doesn't work for parsing mouse input escape sequences, which
48 | # consist of a fixed number of bytes.)
49 | self._stdin_reader = PosixStdinReader(sys.stdin.fileno(), errors='replace')
50 |
51 | def run_command(self, command, pane_id=None):
52 | """
53 | Ask the server to run this command.
54 |
55 | :param pane_id: Optional identifier of the current pane.
56 | """
57 | self._send_packet({
58 | 'cmd': 'run-command',
59 | 'data': command,
60 | 'pane_id': pane_id
61 | })
62 |
63 | def attach(self, detach_other_clients=False, color_depth=ColorDepth.DEPTH_8_BIT):
64 | """
65 | Attach client user interface.
66 | """
67 | assert isinstance(detach_other_clients, bool)
68 |
69 | self._send_size()
70 | self._send_packet({
71 | 'cmd': 'start-gui',
72 | 'detach-others': detach_other_clients,
73 | 'color-depth': color_depth,
74 | 'term': os.environ.get('TERM', ''),
75 | 'data': ''
76 | })
77 |
78 | with raw_mode(sys.stdin.fileno()):
79 | data_buffer = b''
80 |
81 | stdin_fd = sys.stdin.fileno()
82 | socket_fd = self.socket.fileno()
83 | current_timeout = INPUT_TIMEOUT # Timeout, used to flush escape sequences.
84 |
85 | try:
86 | def winch_handler(signum, frame):
87 | self._send_size()
88 |
89 | signal.signal(signal.SIGWINCH, winch_handler)
90 | while True:
91 | r = select_fds([stdin_fd, socket_fd], current_timeout)
92 |
93 | if socket_fd in r:
94 | # Received packet from server.
95 | data = self.socket.recv(1024)
96 |
97 | if data == b'':
98 | # End of file. Connection closed.
99 | # Reset terminal
100 | o = Vt100_Output.from_pty(sys.stdout)
101 | o.quit_alternate_screen()
102 | o.disable_mouse_support()
103 | o.disable_bracketed_paste()
104 | o.reset_attributes()
105 | o.flush()
106 | return
107 | else:
108 | data_buffer += data
109 |
110 | while b'\0' in data_buffer:
111 | pos = data_buffer.index(b'\0')
112 | self._process(data_buffer[:pos])
113 | data_buffer = data_buffer[pos + 1:]
114 |
115 | elif stdin_fd in r:
116 | # Got user input.
117 | self._process_stdin()
118 | current_timeout = INPUT_TIMEOUT
119 |
120 | else:
121 | # Timeout. (Tell the server to flush the vt100 Escape.)
122 | self._send_packet({'cmd': 'flush-input'})
123 | current_timeout = None
124 | finally:
125 | signal.signal(signal.SIGWINCH, signal.SIG_IGN)
126 |
127 | def _process(self, data_buffer):
128 | """
129 | Handle incoming packet from server.
130 | """
131 | packet = json.loads(data_buffer.decode('utf-8'))
132 |
133 | if packet['cmd'] == 'out':
134 | # Call os.write manually. In Python2.6, sys.stdout.write doesn't use UTF-8.
135 | os.write(sys.stdout.fileno(), packet['data'].encode('utf-8'))
136 |
137 | elif packet['cmd'] == 'suspend':
138 | # Suspend client process to background.
139 | if hasattr(signal, 'SIGTSTP'):
140 | os.kill(os.getpid(), signal.SIGTSTP)
141 |
142 | elif packet['cmd'] == 'mode':
143 | # Set terminal to raw/cooked.
144 | action = packet['data']
145 |
146 | if action == 'raw':
147 | cm = raw_mode(sys.stdin.fileno())
148 | cm.__enter__()
149 | self._mode_context_managers.append(cm)
150 |
151 | elif action == 'cooked':
152 | cm = cooked_mode(sys.stdin.fileno())
153 | cm.__enter__()
154 | self._mode_context_managers.append(cm)
155 |
156 | elif action == 'restore' and self._mode_context_managers:
157 | cm = self._mode_context_managers.pop()
158 | cm.__exit__()
159 |
160 | def _process_stdin(self):
161 | """
162 | Received data on stdin. Read and send to server.
163 | """
164 | with nonblocking(sys.stdin.fileno()):
165 | data = self._stdin_reader.read()
166 |
167 | # Send input in chunks of 4k.
168 | step = 4056
169 | for i in range(0, len(data), step):
170 | self._send_packet({
171 | 'cmd': 'in',
172 | 'data': data[i:i + step],
173 | })
174 |
175 | def _send_packet(self, data):
176 | " Send to server. "
177 | data = json.dumps(data).encode('utf-8')
178 |
179 | # Be sure that our socket is blocking, otherwise, the send() call could
180 | # raise `BlockingIOError` if the buffer is full.
181 | self.socket.setblocking(1)
182 |
183 | self.socket.send(data + b'\0')
184 |
185 | def _send_size(self):
186 | " Report terminal size to server. "
187 | rows, cols = _get_size(sys.stdout.fileno())
188 | self._send_packet({
189 | 'cmd': 'size',
190 | 'data': [rows, cols]
191 | })
192 |
193 |
194 | def list_clients():
195 | """
196 | List all the servers that are running.
197 | """
198 | p = '%s/pymux.sock.%s.*' % (tempfile.gettempdir(), getpass.getuser())
199 | for path in glob.glob(p):
200 | try:
201 | yield PosixClient(path)
202 | except socket.error:
203 | pass
204 |
--------------------------------------------------------------------------------
/pymux/client/windows.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
3 | from ctypes import byref, windll
4 | from ctypes.wintypes import DWORD
5 | from prompt_toolkit.eventloop import ensure_future, From
6 | from prompt_toolkit.eventloop import get_event_loop
7 | from prompt_toolkit.input.win32 import Win32Input
8 | from prompt_toolkit.output import ColorDepth
9 | from prompt_toolkit.output.win32 import Win32Output
10 | from prompt_toolkit.win32_types import STD_OUTPUT_HANDLE
11 | import json
12 | import os
13 | import sys
14 |
15 | from ..pipes.win32_client import PipeClient
16 | from .base import Client
17 |
18 | __all__ = [
19 | 'WindowsClient',
20 | 'list_clients',
21 | ]
22 |
23 | # See: https://msdn.microsoft.com/pl-pl/library/windows/desktop/ms686033(v=vs.85).aspx
24 | ENABLE_PROCESSED_INPUT = 0x0001
25 | ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
26 |
27 |
28 | class WindowsClient(Client):
29 | def __init__(self, pipe_name):
30 | self._input = Win32Input()
31 | self._hconsole = windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE)
32 | self._data_buffer = b''
33 |
34 | self.pipe = PipeClient(pipe_name)
35 |
36 | def attach(self, detach_other_clients=False, color_depth=ColorDepth.DEPTH_8_BIT):
37 | assert isinstance(detach_other_clients, bool)
38 | self._send_size()
39 | self._send_packet({
40 | 'cmd': 'start-gui',
41 | 'detach-others': detach_other_clients,
42 | 'color-depth': color_depth,
43 | 'term': os.environ.get('TERM', ''),
44 | 'data': ''
45 | })
46 |
47 | f = ensure_future(self._start_reader())
48 | with self._input.attach(self._input_ready):
49 | # Run as long as we have a connection with the server.
50 | get_event_loop().run_until_complete(f) # Run forever.
51 |
52 | def _start_reader(self):
53 | """
54 | Read messages from the Win32 pipe server and handle them.
55 | """
56 | while True:
57 | message = yield From(self.pipe.read_message())
58 | self._process(message)
59 |
60 | def _process(self, data_buffer):
61 | """
62 | Handle incoming packet from server.
63 | """
64 | packet = json.loads(data_buffer)
65 |
66 | if packet['cmd'] == 'out':
67 | # Call os.write manually. In Python2.6, sys.stdout.write doesn't use UTF-8.
68 | original_mode = DWORD(0)
69 | windll.kernel32.GetConsoleMode(self._hconsole, byref(original_mode))
70 |
71 | windll.kernel32.SetConsoleMode(self._hconsole, DWORD(
72 | ENABLE_PROCESSED_INPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING))
73 |
74 | try:
75 | os.write(sys.stdout.fileno(), packet['data'].encode('utf-8'))
76 | finally:
77 | windll.kernel32.SetConsoleMode(self._hconsole, original_mode)
78 |
79 | elif packet['cmd'] == 'suspend':
80 | # Suspend client process to background.
81 | pass
82 |
83 | elif packet['cmd'] == 'mode':
84 | pass
85 |
86 | # # Set terminal to raw/cooked.
87 | # action = packet['data']
88 |
89 | # if action == 'raw':
90 | # cm = raw_mode(sys.stdin.fileno())
91 | # cm.__enter__()
92 | # self._mode_context_managers.append(cm)
93 |
94 | # elif action == 'cooked':
95 | # cm = cooked_mode(sys.stdin.fileno())
96 | # cm.__enter__()
97 | # self._mode_context_managers.append(cm)
98 |
99 | # elif action == 'restore' and self._mode_context_managers:
100 | # cm = self._mode_context_managers.pop()
101 | # cm.__exit__()
102 |
103 | def _input_ready(self):
104 | keys = self._input.read_keys()
105 | if keys:
106 | self._send_packet({
107 | 'cmd': 'in',
108 | 'data': ''.join(key_press.data for key_press in keys),
109 | })
110 |
111 | def _send_packet(self, data):
112 | " Send to server. "
113 | data = json.dumps(data)
114 | ensure_future(self.pipe.write_message(data))
115 |
116 | def _send_size(self):
117 | " Report terminal size to server. "
118 | output = Win32Output(sys.stdout)
119 | rows, cols = output.get_size()
120 |
121 | self._send_packet({
122 | 'cmd': 'size',
123 | 'data': [rows, cols]
124 | })
125 |
126 |
127 | def list_clients():
128 | return []
129 |
--------------------------------------------------------------------------------
/pymux/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prompt-toolkit/pymux/163eeb31e516b0f6356af46082e8e7aea708386b/pymux/commands/__init__.py
--------------------------------------------------------------------------------
/pymux/commands/aliases.py:
--------------------------------------------------------------------------------
1 | """
2 | Aliases for all commands.
3 | (On purpose kept compatible with tmux.)
4 | """
5 | from __future__ import unicode_literals
6 |
7 | __all__ = (
8 | 'ALIASES',
9 | )
10 |
11 |
12 | ALIASES = {
13 | 'bind': 'bind-key',
14 | 'breakp': 'break-pane',
15 | 'clearhist': 'clear-history',
16 | 'confirm': 'confirm-before',
17 | 'detach': 'detach-client',
18 | 'display': 'display-message',
19 | 'displayp': 'display-panes',
20 | 'killp': 'kill-pane',
21 | 'killw': 'kill-window',
22 | 'last': 'last-window',
23 | 'lastp': 'last-pane',
24 | 'lextl': 'next-layout',
25 | 'lsk': 'list-keys',
26 | 'lsp': 'list-panes',
27 | 'movew': 'move-window',
28 | 'neww': 'new-window',
29 | 'next': 'next-window',
30 | 'pasteb': 'paste-buffer',
31 | 'prev': 'previous-window',
32 | 'prevl': 'previous-layout',
33 | 'rename': 'rename-session',
34 | 'renamew': 'rename-window',
35 | 'resizep': 'resize-pane',
36 | 'rotatew': 'rotate-window',
37 | 'selectl': 'select-layout',
38 | 'selectp': 'select-pane',
39 | 'selectw': 'select-window',
40 | 'send': 'send-keys',
41 | 'set': 'set-option',
42 | 'setw': 'set-window-option',
43 | 'source': 'source-file',
44 | 'splitw': 'split-window',
45 | 'suspendc': 'suspend-client',
46 | 'swapp': 'swap-pane',
47 | 'unbind': 'unbind-key',
48 | }
49 |
--------------------------------------------------------------------------------
/pymux/commands/commands.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 | import docopt
3 | import os
4 | import re
5 | import shlex
6 | import six
7 |
8 | from prompt_toolkit.application.current import get_app
9 | from prompt_toolkit.document import Document
10 | from prompt_toolkit.key_binding.vi_state import InputMode
11 |
12 | from pymux.arrangement import LayoutTypes
13 | from pymux.commands.aliases import ALIASES
14 | from pymux.commands.utils import wrap_argument
15 | from pymux.format import format_pymux_string
16 | from pymux.key_mappings import pymux_key_to_prompt_toolkit_key_sequence, prompt_toolkit_key_to_vt100_key
17 | from pymux.layout import focus_right, focus_left, focus_up, focus_down
18 | from pymux.log import logger
19 | from pymux.options import SetOptionError
20 |
21 | __all__ = (
22 | 'call_command_handler',
23 | 'get_documentation_for_command',
24 | 'get_option_flags_for_command',
25 | 'handle_command',
26 | 'has_command_handler',
27 | )
28 |
29 | COMMANDS_TO_HANDLERS = {} # Global mapping of pymux commands to their handlers.
30 | COMMANDS_TO_HELP = {}
31 | COMMANDS_TO_OPTION_FLAGS = {}
32 |
33 |
34 | def has_command_handler(command):
35 | return command in COMMANDS_TO_HANDLERS
36 |
37 |
38 | def get_documentation_for_command(command):
39 | """ Return the help text for this command, or None if the command is not
40 | known. """
41 | if command in COMMANDS_TO_HELP:
42 | return 'Usage: %s %s' % (command, COMMANDS_TO_HELP.get(command, ''))
43 |
44 |
45 | def get_option_flags_for_command(command):
46 | " Return a list of options (-x flags) for this command. "
47 | return COMMANDS_TO_OPTION_FLAGS.get(command, [])
48 |
49 |
50 | def handle_command(pymux, input_string):
51 | """
52 | Handle command.
53 | """
54 | assert isinstance(input_string, six.text_type)
55 |
56 | input_string = input_string.strip()
57 | logger.info('handle command: %s %s.', input_string, type(input_string))
58 |
59 | if input_string and not input_string.startswith('#'): # Ignore comments.
60 | try:
61 | if six.PY2:
62 | # In Python2.6, shlex doesn't work with unicode input at all.
63 | # In Python2.7, shlex tries to encode using ASCII.
64 | parts = shlex.split(input_string.encode('utf-8'))
65 | parts = [p.decode('utf-8') for p in parts]
66 | else:
67 | parts = shlex.split(input_string)
68 | except ValueError as e:
69 | # E.g. missing closing quote.
70 | pymux.show_message('Invalid command %s: %s' % (input_string, e))
71 | else:
72 | call_command_handler(parts[0], pymux, parts[1:])
73 |
74 |
75 | def call_command_handler(command, pymux, arguments):
76 | """
77 | Execute command.
78 |
79 | :param arguments: List of options.
80 | """
81 | assert isinstance(arguments, list)
82 |
83 | # Resolve aliases.
84 | command = ALIASES.get(command, command)
85 |
86 | try:
87 | handler = COMMANDS_TO_HANDLERS[command]
88 | except KeyError:
89 | pymux.show_message('Invalid command: %s' % (command,))
90 | else:
91 | try:
92 | handler(pymux, arguments)
93 | except CommandException as e:
94 | pymux.show_message(e.message)
95 |
96 |
97 | def cmd(name, options=''):
98 | """
99 | Decorator for all commands.
100 |
101 | Commands will receive (pymux, variables) as input.
102 | Commands can raise CommandException.
103 | """
104 | # Validate options.
105 | if options:
106 | try:
107 | docopt.docopt('Usage:\n %s %s' % (name, options, ), [])
108 | except SystemExit:
109 | pass
110 |
111 | def decorator(func):
112 | def command_wrapper(pymux, arguments):
113 | # Hack to make the 'bind-key' option work.
114 | # (bind-key expects a variable number of arguments.)
115 | if name == 'bind-key' and '--' not in arguments:
116 | # Insert a double dash after the first non-option.
117 | for i, p in enumerate(arguments):
118 | if not p.startswith('-'):
119 | arguments.insert(i + 1, '--')
120 | break
121 |
122 | # Parse options.
123 | try:
124 | # Python 2 workaround: pass bytes to docopt.
125 | # From the following, only the bytes version returns the right
126 | # output in Python 2:
127 | # docopt.docopt('Usage:\n app ...', [b'a', b'b'])
128 | # docopt.docopt('Usage:\n app ...', [u'a', u'b'])
129 | # https://github.com/docopt/docopt/issues/30
130 | # (Not sure how reliable this is...)
131 | if six.PY2:
132 | arguments = [a.encode('utf-8') for a in arguments]
133 |
134 | received_options = docopt.docopt(
135 | 'Usage:\n %s %s' % (name, options),
136 | arguments,
137 | help=False) # Don't interpret the '-h' option as help.
138 |
139 | # Make sure that all the received options from docopt are
140 | # unicode objects. (Docopt returns 'str' for Python2.)
141 | for k, v in received_options.items():
142 | if isinstance(v, six.binary_type):
143 | received_options[k] = v.decode('utf-8')
144 | except SystemExit:
145 | raise CommandException('Usage: %s %s' % (name, options))
146 |
147 | # Call handler.
148 | func(pymux, received_options)
149 |
150 | # Invalidate all clients, not just the current CLI.
151 | pymux.invalidate()
152 |
153 | COMMANDS_TO_HANDLERS[name] = command_wrapper
154 | COMMANDS_TO_HELP[name] = options
155 |
156 | # Get list of option flags.
157 | flags = re.findall(r'-[a-zA-Z0-9]\b', options)
158 | COMMANDS_TO_OPTION_FLAGS[name] = flags
159 |
160 | return func
161 | return decorator
162 |
163 |
164 | class CommandException(Exception):
165 | " When raised from a command handler, this message will be shown. "
166 | def __init__(self, message):
167 | self.message = message
168 |
169 | #
170 | # The actual commands.
171 | #
172 |
173 |
174 | @cmd('break-pane', options='[-d]')
175 | def break_pane(pymux, variables):
176 | dont_focus_window = variables['-d']
177 |
178 | pymux.arrangement.break_pane(set_active=not dont_focus_window)
179 | pymux.invalidate()
180 |
181 |
182 | @cmd('select-pane', options='(-L|-R|-U|-D|-t )')
183 | def select_pane(pymux, variables):
184 |
185 | if variables['-t']:
186 | pane_id = variables['']
187 | w = pymux.arrangement.get_active_window()
188 |
189 | if pane_id == ':.+':
190 | w.focus_next()
191 | elif pane_id == ':.-':
192 | w.focus_previous()
193 | else:
194 | # Select pane by index.
195 | try:
196 | pane_id = int(pane_id[1:])
197 | w.active_pane = w.panes[pane_id]
198 | except (IndexError, ValueError):
199 | raise CommandException('Invalid pane.')
200 |
201 | else:
202 | if variables['-L']: h = focus_left
203 | if variables['-U']: h = focus_up
204 | if variables['-D']: h = focus_down
205 | if variables['-R']: h = focus_right
206 |
207 | h(pymux)
208 |
209 |
210 | @cmd('select-window', options='(-t )')
211 | def select_window(pymux, variables):
212 | """
213 | Select a window. E.g: select-window -t :3
214 | """
215 | window_id = variables['']
216 |
217 | def invalid_window():
218 | raise CommandException('Invalid window: %s' % window_id)
219 |
220 | if window_id.startswith(':'):
221 | try:
222 | number = int(window_id[1:])
223 | except ValueError:
224 | invalid_window()
225 | else:
226 | w = pymux.arrangement.get_window_by_index(number)
227 | if w:
228 | pymux.arrangement.set_active_window(w)
229 | else:
230 | invalid_window()
231 | else:
232 | invalid_window()
233 |
234 |
235 | @cmd('move-window', options='(-t )')
236 | def move_window(pymux, variables):
237 | """
238 | Move window to a new index.
239 | """
240 | dst_window = variables['']
241 | try:
242 | new_index = int(dst_window)
243 | except ValueError:
244 | raise CommandException('Invalid window index: %r' % (dst_window, ))
245 |
246 | # Check first whether the index was not yet taken.
247 | if pymux.arrangement.get_window_by_index(new_index):
248 | raise CommandException("Can't move window: index in use.")
249 |
250 | # Save index.
251 | w = pymux.arrangement.get_active_window()
252 | pymux.arrangement.move_window(w, new_index)
253 |
254 |
255 | @cmd('rotate-window', options='[-D|-U]')
256 | def rotate_window(pymux, variables):
257 | if variables['-D']:
258 | pymux.arrangement.rotate_window(count=-1)
259 | else:
260 | pymux.arrangement.rotate_window()
261 |
262 |
263 | @cmd('swap-pane', options='(-D|-U)')
264 | def swap_pane(pymux, variables):
265 | pymux.arrangement.get_active_window().rotate(with_pane_after_only=variables['-U'])
266 |
267 |
268 | @cmd('kill-pane')
269 | def kill_pane(pymux, variables):
270 | pane = pymux.arrangement.get_active_pane()
271 | pymux.kill_pane(pane)
272 |
273 |
274 | @cmd('kill-window')
275 | def kill_window(pymux, variables):
276 | " Kill all panes in the current window. "
277 | for pane in pymux.arrangement.get_active_window().panes:
278 | pymux.kill_pane(pane)
279 |
280 |
281 | @cmd('suspend-client')
282 | def suspend_client(pymux, variables):
283 | connection = pymux.get_connection()
284 |
285 | if connection:
286 | connection.suspend_client_to_background()
287 |
288 |
289 | @cmd('clock-mode')
290 | def clock_mode(pymux, variables):
291 | pane = pymux.arrangement.get_active_pane()
292 | if pane:
293 | pane.clock_mode = not pane.clock_mode
294 |
295 |
296 | @cmd('last-pane')
297 | def last_pane(pymux, variables):
298 | w = pymux.arrangement.get_active_window()
299 | prev_active_pane = w.previous_active_pane
300 |
301 | if prev_active_pane:
302 | w.active_pane = prev_active_pane
303 |
304 |
305 | @cmd('next-layout')
306 | def next_layout(pymux, variables):
307 | " Select next layout. "
308 | pane = pymux.arrangement.get_active_window()
309 | if pane:
310 | pane.select_next_layout()
311 |
312 |
313 | @cmd('previous-layout')
314 | def previous_layout(pymux, variables):
315 | " Select previous layout. "
316 | pane = pymux.arrangement.get_active_window()
317 | if pane:
318 | pane.select_previous_layout()
319 |
320 |
321 | @cmd('new-window', options='[(-n )] [(-c )] []')
322 | def new_window(pymux, variables):
323 | executable = variables['']
324 | start_directory = variables['']
325 | name = variables['']
326 |
327 | pymux.create_window(executable, start_directory=start_directory, name=name)
328 |
329 |
330 | @cmd('next-window')
331 | def next_window(pymux, variables):
332 | " Focus the next window. "
333 | pymux.arrangement.focus_next_window()
334 |
335 |
336 | @cmd('last-window')
337 | def _(pymux, variables):
338 | " Go to previous active window. "
339 | w = pymux.arrangement.get_previous_active_window()
340 |
341 | if w:
342 | pymux.arrangement.set_active_window(w)
343 |
344 |
345 | @cmd('previous-window')
346 | def previous_window(pymux, variables):
347 | " Focus the previous window. "
348 | pymux.arrangement.focus_previous_window()
349 |
350 |
351 | @cmd('select-layout', options='')
352 | def select_layout(pymux, variables):
353 | layout_type = variables['']
354 |
355 | if layout_type in LayoutTypes._ALL:
356 | pymux.arrangement.get_active_window().select_layout(layout_type)
357 | else:
358 | raise CommandException('Invalid layout type.')
359 |
360 |
361 | @cmd('rename-window', options='')
362 | def rename_window(pymux, variables):
363 | """
364 | Rename the active window.
365 | """
366 | pymux.arrangement.get_active_window().chosen_name = variables['']
367 |
368 |
369 | @cmd('rename-pane', options='')
370 | def rename_pane(pymux, variables):
371 | """
372 | Rename the active pane.
373 | """
374 | pymux.arrangement.get_active_pane().chosen_name = variables['']
375 |
376 |
377 | @cmd('rename-session', options='')
378 | def rename_session(pymux, variables):
379 | """
380 | Rename this session.
381 | """
382 | pymux.session_name = variables['']
383 |
384 |
385 | @cmd('split-window', options='[-v|-h] [(-c )] []')
386 | def split_window(pymux, variables):
387 | """
388 | Split horizontally or vertically.
389 | """
390 | executable = variables['']
391 | start_directory = variables['']
392 |
393 | # The tmux definition of horizontal is the opposite of prompt_toolkit.
394 | pymux.add_process(executable, vsplit=variables['-h'],
395 | start_directory=start_directory)
396 |
397 |
398 | @cmd('resize-pane', options="[(-L )] [(-U )] [(-D )] [(-R )] [-Z]")
399 | def resize_pane(pymux, variables):
400 | """
401 | Resize/zoom the active pane.
402 | """
403 | try:
404 | left = int(variables[''] or 0)
405 | right = int(variables[''] or 0)
406 | up = int(variables[''] or 0)
407 | down = int(variables[''] or 0)
408 | except ValueError:
409 | raise CommandException('Expecting an integer.')
410 |
411 | w = pymux.arrangement.get_active_window()
412 |
413 | if w:
414 | w.change_size_for_active_pane(up=up, right=right, down=down, left=left)
415 |
416 | # Zoom in/out.
417 | if variables['-Z']:
418 | w.zoom = not w.zoom
419 |
420 |
421 | @cmd('detach-client')
422 | def detach_client(pymux, variables):
423 | """
424 | Detach client.
425 | """
426 | pymux.detach_client(get_app())
427 |
428 |
429 | @cmd('confirm-before', options='[(-p )] ')
430 | def confirm_before(pymux, variables):
431 | client_state = pymux.get_client_state()
432 |
433 | client_state.confirm_text = variables[''] or ''
434 | client_state.confirm_command = variables['']
435 |
436 |
437 | @cmd('command-prompt', options='[(-p )] [(-I )] []')
438 | def command_prompt(pymux, variables):
439 | """
440 | Enter command prompt.
441 | """
442 | client_state = pymux.get_client_state()
443 |
444 | if variables['']:
445 | # When a 'command' has been given.
446 | client_state.prompt_text = variables[''] or '(%s)' % variables[''].split()[0]
447 | client_state.prompt_command = variables['']
448 |
449 | client_state.prompt_mode = True
450 | client_state.prompt_buffer.reset(Document(
451 | format_pymux_string(pymux, variables[''] or '')))
452 |
453 | get_app().layout.focus(client_state.prompt_buffer)
454 | else:
455 | # Show the ':' prompt.
456 | client_state.prompt_text = ''
457 | client_state.prompt_command = ''
458 |
459 | get_app().layout.focus(client_state.command_buffer)
460 |
461 | # Go to insert mode.
462 | get_app().vi_state.input_mode = InputMode.INSERT
463 |
464 |
465 | @cmd('send-prefix')
466 | def send_prefix(pymux, variables):
467 | """
468 | Send prefix to active pane.
469 | """
470 | process = pymux.arrangement.get_active_pane().process
471 |
472 | for k in pymux.key_bindings_manager.prefix:
473 | vt100_data = prompt_toolkit_key_to_vt100_key(k)
474 | process.write_input(vt100_data)
475 |
476 |
477 | @cmd('bind-key', options='[-n] [--] [...]')
478 | def bind_key(pymux, variables):
479 | """
480 | Bind a key sequence.
481 | -n: Not necessary to use the prefix.
482 | """
483 | key = variables['']
484 | command = variables['']
485 | arguments = variables['']
486 | needs_prefix = not variables['-n']
487 |
488 | try:
489 | pymux.key_bindings_manager.add_custom_binding(
490 | key, command, arguments, needs_prefix=needs_prefix)
491 | except ValueError:
492 | raise CommandException('Invalid key: %r' % (key, ))
493 |
494 |
495 | @cmd('unbind-key', options='[-n] ')
496 | def unbind_key(pymux, variables):
497 | """
498 | Remove key binding.
499 | """
500 | key = variables['']
501 | needs_prefix = not variables['-n']
502 |
503 | pymux.key_bindings_manager.remove_custom_binding(
504 | key, needs_prefix=needs_prefix)
505 |
506 |
507 | @cmd('send-keys', options='...')
508 | def send_keys(pymux, variables):
509 | """
510 | Send key strokes to the active process.
511 | """
512 | pane = pymux.arrangement.get_active_pane()
513 |
514 | if pane.display_scroll_buffer:
515 | raise CommandException('Cannot send keys. Pane is in copy mode.')
516 |
517 | for key in variables['']:
518 | # Translate key from pymux key to prompt_toolkit key.
519 | try:
520 | keys_sequence = pymux_key_to_prompt_toolkit_key_sequence(key)
521 | except ValueError:
522 | raise CommandException('Invalid key: %r' % (key, ))
523 |
524 | # Translate prompt_toolkit key to VT100 key.
525 | for k in keys_sequence:
526 | pane.process.write_key(k)
527 |
528 |
529 | @cmd('copy-mode', options='[-u]')
530 | def copy_mode(pymux, variables):
531 | """
532 | Enter copy mode.
533 | """
534 | go_up = variables['-u'] # Go in copy mode and page-up directly.
535 | # TODO: handle '-u'
536 |
537 | pane = pymux.arrangement.get_active_pane()
538 | pane.enter_copy_mode()
539 |
540 |
541 | @cmd('paste-buffer')
542 | def paste_buffer(pymux, variables):
543 | """
544 | Paste clipboard content into buffer.
545 | """
546 | pane = pymux.arrangement.get_active_pane()
547 | pane.process.write_input(get_app().clipboard.get_data().text, paste=True)
548 |
549 |
550 | @cmd('source-file', options='')
551 | def source_file(pymux, variables):
552 | """
553 | Source configuration file.
554 | """
555 | filename = os.path.expanduser(variables[''])
556 | try:
557 | with open(filename, 'rb') as f:
558 | for line in f:
559 | line = line.decode('utf-8')
560 | handle_command(pymux, line)
561 | except IOError as e:
562 | raise CommandException('IOError: %s' % (e, ))
563 |
564 |
565 | @cmd('set-option', options='