added chat overlay
This commit is contained in:
commit
3275444755
67
README.md
Normal file
67
README.md
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
## [MAPLE|G1MP]+[ML^N]+[NETSPANNING[i]]
|
||||||
|
---
|
||||||
|
```
|
||||||
|
MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
|
||||||
|
MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMh+MMMMMMMMMMMMMMhsMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
|
||||||
|
MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMm/ oMMMMMMMMMMMMMMm +NMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
|
||||||
|
MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMy` yMMMMMMMMMMMMMMM- -mMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
|
||||||
|
MMMMMMMMMMMMMMMMMMMMMMMMs+dMMMMMMMMMM+ sMMMMMMMMMMMMMMM- `dMMMMMMMMMMms/NMMMMMMMMMMMMMMMMMMMMMMM
|
||||||
|
MMMMMMMMMMMMMMMMMMMMMMMMM+ .omMMMMMM: -MMMMMMMMMMMMMMo `yMMMMMMMy: `dMMMMMMMMMMMMMMMMMMMMMMMM
|
||||||
|
MMMMMMMMMMMMMMMMMMMMMMMMMM- /dMMM+ sMMMMMMMMMMMMh `hMMMNo` sMMMMMMMMMMMMMMMMMMMMMMMMM
|
||||||
|
MMMMMMMMMMMMMMMMMMMMMMMMMMd :dm `mMMMMMMMMMMN. .NNo` .MMMMMMMMMMMMMMMMMMMMMMMMMM
|
||||||
|
MMMMMMMMMMMMMMMMMMMMMMMMMMM: - :MMMMMMMMMMs :` sMMMMMMMMMMMMMMMMMMMMMMMMMM
|
||||||
|
MMMMMMMMMMMMMMMMMMMMMMMMMMMs ymNMMMMMNm. NMMMMMMMMMMMMMMMMMMMMMMMMMM
|
||||||
|
MMMMMMMMMMMMMMMMMMMMMMMMMMMy `-/-` .MMMMMMMMMMMMMMMMMMMMMMMMMMM
|
||||||
|
MMMMMMMMMMMMMMMMMMMMMMMMMMMo .NMMMMMMMMMMMMMMMMMMMMMMMMMM
|
||||||
|
MMMMMMMMMMMMMMMMMMMMMMMNh+. :sdMMMMMMMMMMMMMMMMMMMMMMM
|
||||||
|
MMMMMMMMMMMMMMMMMhso+:. `-/+syMMMMMMMMMMMMMMMMM
|
||||||
|
MMMMMMMMMMMMMMMMM- dMMMMMMMMMMMMMMMM
|
||||||
|
MMMMMMMMMMMMMMMMM` `.:+/. `/s+:. sMMMMMMMMMMMMMMMM
|
||||||
|
MMMMMMMMMMMMMMMNo -oms. .//-` `:/:` `+md+` .hMMMMMMMMMMMMMMM
|
||||||
|
MMMMMMMMMMMMMNs` .odNdo. .ohmd+` :dMMMMMMMMMMMMM
|
||||||
|
MMMMMMMMMMMNo` .. .- :hMMMMMMMMMMM
|
||||||
|
MMMMMMMMMd+` -sNMMMMMMMM
|
||||||
|
MMMMMMNs- `.. `/-. `+dMMMMMM
|
||||||
|
MMMNy: ./sdNMMMh: `sNMMMNds/. .odMMM
|
||||||
|
MM+ :ymMMMMMMMMMMh. +NMMMMMMMMMMmo- /NM
|
||||||
|
MMMh: .sNMMMMMMMMMMMMMMN- `hMMMMMMMMMMMMMMMm+` :hMMM
|
||||||
|
MMMMMd:` ``-:+shmMMMMMMMMMMMMMMMMMMN. hMMMMMMMMMMMMMMMMMMMmhs+/-..``````./dMMMMM
|
||||||
|
MMMMMMMMMNNNNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMo .MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
|
||||||
|
MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMy .MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
|
||||||
|
MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMN. /MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
|
||||||
|
MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMN+` `+NMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
|
||||||
|
MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNs. -hMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
|
||||||
|
MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMdyymMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
|
||||||
|
```
|
||||||
|
---
|
||||||
|
## Summary
|
||||||
|
base: `https://github.com/prompt-toolkit/pymux`
|
||||||
|
```
|
||||||
|
tailoring a multiplexor for personal usage. using pymux for the base code, it's a tmux python port.
|
||||||
|
```
|
||||||
|
---
|
||||||
|
## Changelog - v0.1
|
||||||
|
- added own chat window, and wrote in an an arbitrary data poplulator until fd socks are networked.
|
||||||
|
---
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
###### [ substitute apt for the package manager of your choice ]
|
||||||
|
|
||||||
|
- `apt install python3` - `*note: python3.9 is ideal`
|
||||||
|
- `apt install python3-pip`
|
||||||
|
- `python3 -m pip install virtualenv`
|
||||||
|
---
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
###### - [ virtualenv could be substituted for pyenv/pipenv or no virtualenv et al ]
|
||||||
|
- `git clone --recursive https://git.tcp.direct/decoded/maple_netspan.git`
|
||||||
|
- `cd dr1p_pymux`
|
||||||
|
- `virtualenv -p python3.9 env`
|
||||||
|
- `source env/bin/activate`
|
||||||
|
- `pip install -r requirements.txt`
|
||||||
|
---
|
||||||
|
# Usage
|
||||||
|
## 1 - `cd dr1p_pymux`
|
||||||
|
## 2 - `source env/bin/activate`
|
||||||
|
## 3 - `python3.9 dr1p_pymux.py`
|
||||||
|
---
|
18
dr1p_pymux.py
Normal file
18
dr1p_pymux.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
###################################################################################### SOF
|
||||||
|
class DR1PP1NG():
|
||||||
|
color_depth="DEPTH_24_BIT"
|
||||||
|
def __init__(self,mode):
|
||||||
|
self.__mode__(mode)
|
||||||
|
def __mode__(self,mode):
|
||||||
|
if mode==1:
|
||||||
|
from pymux.main import Pymux
|
||||||
|
self.dr1pp1ng=Pymux()
|
||||||
|
self.dr1pp1ng.run_standalone(self.color_depth)
|
||||||
|
elif mode==2:
|
||||||
|
from pymux.entry_points.run_pymux import run
|
||||||
|
run()
|
||||||
|
##########################################################################################
|
||||||
|
if __name__=="__main__":
|
||||||
|
dr1pp1ng=DR1PP1NG(2)
|
||||||
|
#dr1pp1ng=DR1PP1NG(1)
|
||||||
|
###################################################################################### EOF
|
1
pymux/__init__.py
Normal file
1
pymux/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from __future__ import unicode_literals
|
8
pymux/__main__.py
Normal file
8
pymux/__main__.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
"""
|
||||||
|
Make sure `python -m pymux` works.
|
||||||
|
"""
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from .entry_points.run_pymux import run
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run()
|
737
pymux/arrangement.py
Normal file
737
pymux/arrangement.py
Normal file
@ -0,0 +1,737 @@
|
|||||||
|
"""
|
||||||
|
Arrangement of panes.
|
||||||
|
|
||||||
|
Don't confuse with the prompt_toolkit VSplit/HSplit classes. This is a higher
|
||||||
|
level abstraction of the Pymux window layout.
|
||||||
|
|
||||||
|
An arrangement consists of a list of windows. And a window has a list of panes,
|
||||||
|
arranged by ordering them in HSplit/VSplit instances.
|
||||||
|
"""
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from ptterm import Terminal
|
||||||
|
from prompt_toolkit.application.current import get_app, set_app
|
||||||
|
from prompt_toolkit.buffer import Buffer
|
||||||
|
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import weakref
|
||||||
|
import six
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'LayoutTypes',
|
||||||
|
'Pane',
|
||||||
|
'HSplit',
|
||||||
|
'VSplit',
|
||||||
|
'Window',
|
||||||
|
'Arrangement',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LayoutTypes:
|
||||||
|
# The values are in lowercase with dashes, because that is what users can
|
||||||
|
# use at the command line.
|
||||||
|
EVEN_HORIZONTAL = 'even-horizontal'
|
||||||
|
EVEN_VERTICAL = 'even-vertical'
|
||||||
|
MAIN_HORIZONTAL = 'main-horizontal'
|
||||||
|
MAIN_VERTICAL = 'main-vertical'
|
||||||
|
TILED = 'tiled'
|
||||||
|
|
||||||
|
_ALL = [EVEN_HORIZONTAL, EVEN_VERTICAL, MAIN_HORIZONTAL, MAIN_VERTICAL, TILED]
|
||||||
|
|
||||||
|
|
||||||
|
class Pane(object):
|
||||||
|
"""
|
||||||
|
One pane, containing one process and a search buffer for going into copy
|
||||||
|
mode or displaying the help.
|
||||||
|
"""
|
||||||
|
_pane_counter = 1000 # Start at 1000, to be sure to not confuse this with pane indexes.
|
||||||
|
|
||||||
|
def __init__(self, terminal=None):
|
||||||
|
assert isinstance(terminal, Terminal)
|
||||||
|
|
||||||
|
self.terminal = terminal
|
||||||
|
self.chosen_name = None
|
||||||
|
|
||||||
|
# Displayed the clock instead of this pane content.
|
||||||
|
self.clock_mode = False
|
||||||
|
|
||||||
|
# Give unique ID.
|
||||||
|
Pane._pane_counter += 1
|
||||||
|
self.pane_id = Pane._pane_counter
|
||||||
|
|
||||||
|
# Prompt_toolkit buffer, for displaying scrollable text.
|
||||||
|
# (In copy mode, or help mode.)
|
||||||
|
# Note: Because the scroll_buffer can only contain text, we also use the
|
||||||
|
# get_tokens_for_line, that returns the token list with color
|
||||||
|
# information for each line.
|
||||||
|
self.scroll_buffer = Buffer(read_only=True)
|
||||||
|
self.copy_get_tokens_for_line = lambda lineno: []
|
||||||
|
self.display_scroll_buffer = False
|
||||||
|
self.scroll_buffer_title = ''
|
||||||
|
|
||||||
|
@property
|
||||||
|
def process(self):
|
||||||
|
return self.terminal.process
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""
|
||||||
|
The name for the window as displayed in the title bar and status bar.
|
||||||
|
"""
|
||||||
|
# Name, explicitely set for the pane.
|
||||||
|
if self.chosen_name:
|
||||||
|
return self.chosen_name
|
||||||
|
else:
|
||||||
|
# Name from the process running inside the pane.
|
||||||
|
name = self.process.get_name()
|
||||||
|
if name:
|
||||||
|
return os.path.basename(name)
|
||||||
|
|
||||||
|
return ''
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
self.terminal.enter_copy_mode()
|
||||||
|
|
||||||
|
def focus(self):
|
||||||
|
"""
|
||||||
|
Focus this pane.
|
||||||
|
"""
|
||||||
|
get_app().layout.focus(self.terminal)
|
||||||
|
|
||||||
|
|
||||||
|
class _WeightsDictionary(weakref.WeakKeyDictionary):
|
||||||
|
"""
|
||||||
|
Dictionary for the weights: weak keys, but defaults to 1.
|
||||||
|
|
||||||
|
(Weights are used to represent the proportion of pane sizes in
|
||||||
|
HSplit/VSplit lists.)
|
||||||
|
|
||||||
|
This dictionary maps the child (another HSplit/VSplit or Pane), to the
|
||||||
|
size. (Integer.)
|
||||||
|
"""
|
||||||
|
def __getitem__(self, key):
|
||||||
|
try:
|
||||||
|
# (Don't use 'super' here. This is a classobj in Python2.)
|
||||||
|
return weakref.WeakKeyDictionary.__getitem__(self, key)
|
||||||
|
except KeyError:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
class _Split(list):
|
||||||
|
"""
|
||||||
|
Base class for horizontal and vertical splits. (This is a higher level
|
||||||
|
split than prompt_toolkit.layout.HSplit.)
|
||||||
|
"""
|
||||||
|
def __init__(self, *a, **kw):
|
||||||
|
list.__init__(self, *a, **kw)
|
||||||
|
|
||||||
|
# Mapping children to its weight.
|
||||||
|
self.weights = _WeightsDictionary()
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
# Required in order to add HSplit/VSplit to the weights dict. "
|
||||||
|
return id(self)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '%s(%s)' % (self.__class__.__name__, list.__repr__(self))
|
||||||
|
|
||||||
|
|
||||||
|
class HSplit(_Split):
|
||||||
|
""" Horizontal split. """
|
||||||
|
|
||||||
|
|
||||||
|
class VSplit(_Split):
|
||||||
|
""" Horizontal split. """
|
||||||
|
|
||||||
|
|
||||||
|
class Window(object):
|
||||||
|
"""
|
||||||
|
Pymux window.
|
||||||
|
"""
|
||||||
|
_window_counter = 1000 # Start here, to avoid confusion with window index.
|
||||||
|
|
||||||
|
def __init__(self, index=0):
|
||||||
|
self.index = index
|
||||||
|
self.root = HSplit()
|
||||||
|
self._active_pane = None
|
||||||
|
self._prev_active_pane = None
|
||||||
|
self.chosen_name = None
|
||||||
|
self.previous_selected_layout = None
|
||||||
|
|
||||||
|
#: When true, the current pane is zoomed in.
|
||||||
|
self.zoom = False
|
||||||
|
|
||||||
|
#: When True, send input to all panes simultaniously.
|
||||||
|
self.synchronize_panes = False
|
||||||
|
|
||||||
|
# Give unique ID.
|
||||||
|
Window._window_counter += 1
|
||||||
|
self.window_id = Window._window_counter
|
||||||
|
|
||||||
|
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 '<empty-window>'
|
||||||
|
|
||||||
|
def _hash_for_split(split):
|
||||||
|
result = []
|
||||||
|
for item in split:
|
||||||
|
if isinstance(item, (VSplit, HSplit)):
|
||||||
|
result.append(_hash_for_split(item))
|
||||||
|
elif isinstance(item, Pane):
|
||||||
|
result.append('p%s' % item.pane_id)
|
||||||
|
|
||||||
|
if isinstance(split, HSplit):
|
||||||
|
return 'HSplit(%s)' % (','.join(result))
|
||||||
|
else:
|
||||||
|
return 'VSplit(%s)' % (','.join(result))
|
||||||
|
|
||||||
|
return '<window_id=%s,zoom=%s,children=%s>' % (
|
||||||
|
self.window_id, self.zoom, _hash_for_split(self.root))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def active_pane(self):
|
||||||
|
"""
|
||||||
|
The current active :class:`.Pane`.
|
||||||
|
"""
|
||||||
|
return self._active_pane
|
||||||
|
|
||||||
|
@active_pane.setter
|
||||||
|
def active_pane(self, value):
|
||||||
|
assert isinstance(value, Pane)
|
||||||
|
|
||||||
|
# Remember previous active pane.
|
||||||
|
if self._active_pane:
|
||||||
|
self._prev_active_pane = weakref.ref(self._active_pane)
|
||||||
|
|
||||||
|
self.zoom = False
|
||||||
|
self._active_pane = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def previous_active_pane(self):
|
||||||
|
"""
|
||||||
|
The previous active :class:`.Pane` or `None` if unknown.
|
||||||
|
"""
|
||||||
|
p = self._prev_active_pane and self._prev_active_pane()
|
||||||
|
|
||||||
|
# Only return when this pane actually still exists in the current
|
||||||
|
# window.
|
||||||
|
if p and p in self.panes:
|
||||||
|
return p
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""
|
||||||
|
The name for this window as it should be displayed in the status bar.
|
||||||
|
"""
|
||||||
|
# Name, explicitely set for the window.
|
||||||
|
if self.chosen_name:
|
||||||
|
return self.chosen_name
|
||||||
|
else:
|
||||||
|
pane = self.active_pane
|
||||||
|
if pane:
|
||||||
|
return pane.name
|
||||||
|
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def add_pane(self, pane, vsplit=False):
|
||||||
|
"""
|
||||||
|
Add another pane to this Window.
|
||||||
|
"""
|
||||||
|
assert isinstance(pane, Pane)
|
||||||
|
assert isinstance(vsplit, bool)
|
||||||
|
|
||||||
|
split_cls = VSplit if vsplit else HSplit
|
||||||
|
|
||||||
|
if self.active_pane is None:
|
||||||
|
self.root.append(pane)
|
||||||
|
else:
|
||||||
|
parent = self._get_parent(self.active_pane)
|
||||||
|
same_direction = isinstance(parent, split_cls)
|
||||||
|
|
||||||
|
index = parent.index(self.active_pane)
|
||||||
|
|
||||||
|
if same_direction:
|
||||||
|
parent.insert(index + 1, pane)
|
||||||
|
else:
|
||||||
|
new_split = split_cls([self.active_pane, pane])
|
||||||
|
parent[index] = new_split
|
||||||
|
|
||||||
|
# Give the newly created split the same weight as the original
|
||||||
|
# pane that was at this position.
|
||||||
|
parent.weights[new_split] = parent.weights[self.active_pane]
|
||||||
|
|
||||||
|
self.active_pane = pane
|
||||||
|
self.zoom = False
|
||||||
|
|
||||||
|
def remove_pane(self, pane):
|
||||||
|
"""
|
||||||
|
Remove pane from this Window.
|
||||||
|
"""
|
||||||
|
assert isinstance(pane, Pane)
|
||||||
|
|
||||||
|
if pane in self.panes:
|
||||||
|
# When this pane was focused, switch to previous active or next in order.
|
||||||
|
if pane == self.active_pane:
|
||||||
|
if self.previous_active_pane:
|
||||||
|
self.active_pane = self.previous_active_pane
|
||||||
|
else:
|
||||||
|
self.focus_next()
|
||||||
|
|
||||||
|
# Remove from the parent. When the parent becomes empty, remove the
|
||||||
|
# parent itself recursively.
|
||||||
|
p = self._get_parent(pane)
|
||||||
|
p.remove(pane)
|
||||||
|
|
||||||
|
while len(p) == 0 and p != self.root:
|
||||||
|
p2 = self._get_parent(p)
|
||||||
|
p2.remove(p)
|
||||||
|
p = p2
|
||||||
|
|
||||||
|
# When the parent has only one item left, collapse into its parent.
|
||||||
|
while len(p) == 1 and p != self.root:
|
||||||
|
p2 = self._get_parent(p)
|
||||||
|
p2.weights[p[0]] = p2.weights[p] # Keep dimensions.
|
||||||
|
i = p2.index(p)
|
||||||
|
p2[i] = p[0]
|
||||||
|
p = p2
|
||||||
|
|
||||||
|
@property
|
||||||
|
def panes(self):
|
||||||
|
" List with all panes from this Window. "
|
||||||
|
result = []
|
||||||
|
|
||||||
|
for s in self.splits:
|
||||||
|
for item in s:
|
||||||
|
if isinstance(item, Pane):
|
||||||
|
result.append(item)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
@property
|
||||||
|
def splits(self):
|
||||||
|
" Return a list with all HSplit/VSplit instances. "
|
||||||
|
result = []
|
||||||
|
|
||||||
|
def collect(split):
|
||||||
|
result.append(split)
|
||||||
|
|
||||||
|
for item in split:
|
||||||
|
if isinstance(item, (HSplit, VSplit)):
|
||||||
|
collect(item)
|
||||||
|
|
||||||
|
collect(self.root)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _get_parent(self, item):
|
||||||
|
" The HSplit/VSplit that contains the active pane. "
|
||||||
|
for s in self.splits:
|
||||||
|
if item in s:
|
||||||
|
return s
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_panes(self):
|
||||||
|
" True when this window contains at least one pane. "
|
||||||
|
return len(self.panes) > 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def active_process(self):
|
||||||
|
" Return `Process` that should receive user input. "
|
||||||
|
p = self.active_pane
|
||||||
|
|
||||||
|
if p is not None:
|
||||||
|
return p.process
|
||||||
|
|
||||||
|
def focus_next(self, count=1):
|
||||||
|
" Focus the next pane. "
|
||||||
|
panes = self.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. "
|
||||||
|
self.focus_next(count=-1)
|
||||||
|
|
||||||
|
def rotate(self, count=1, with_pane_before_only=False, with_pane_after_only=False):
|
||||||
|
"""
|
||||||
|
Rotate panes.
|
||||||
|
When `with_pane_before_only` or `with_pane_after_only` is True, only rotate
|
||||||
|
with the pane before/after the active pane.
|
||||||
|
"""
|
||||||
|
# Create (split, index, pane, weight) tuples.
|
||||||
|
items = []
|
||||||
|
current_pane_index = None
|
||||||
|
|
||||||
|
for s in self.splits:
|
||||||
|
for index, item in enumerate(s):
|
||||||
|
if isinstance(item, Pane):
|
||||||
|
items.append((s, index, item, s.weights[item]))
|
||||||
|
if item == self.active_pane:
|
||||||
|
current_pane_index = len(items) - 1
|
||||||
|
|
||||||
|
# Only before after? Reduce list of panes.
|
||||||
|
if with_pane_before_only:
|
||||||
|
items = items[current_pane_index - 1:current_pane_index + 1]
|
||||||
|
|
||||||
|
elif with_pane_after_only:
|
||||||
|
items = items[current_pane_index:current_pane_index + 2]
|
||||||
|
|
||||||
|
# Rotate positions.
|
||||||
|
for i, triple in enumerate(items):
|
||||||
|
split, index, pane, weight = triple
|
||||||
|
|
||||||
|
new_item = items[(i + count) % len(items)][2]
|
||||||
|
|
||||||
|
split[index] = new_item
|
||||||
|
split.weights[new_item] = weight
|
||||||
|
|
||||||
|
def select_layout(self, layout_type):
|
||||||
|
"""
|
||||||
|
Select one of the predefined layouts.
|
||||||
|
"""
|
||||||
|
assert layout_type in LayoutTypes._ALL
|
||||||
|
|
||||||
|
# When there is only one pane, always choose EVEN_HORIZONTAL,
|
||||||
|
# Otherwise, we create VSplit/HSplit instances with an empty list of
|
||||||
|
# children.
|
||||||
|
if len(self.panes) == 1:
|
||||||
|
layout_type = LayoutTypes.EVEN_HORIZONTAL
|
||||||
|
|
||||||
|
# even-horizontal.
|
||||||
|
if layout_type == LayoutTypes.EVEN_HORIZONTAL:
|
||||||
|
self.root = HSplit(self.panes)
|
||||||
|
|
||||||
|
# even-vertical.
|
||||||
|
elif layout_type == LayoutTypes.EVEN_VERTICAL:
|
||||||
|
self.root = VSplit(self.panes)
|
||||||
|
|
||||||
|
# main-horizontal.
|
||||||
|
elif layout_type == LayoutTypes.MAIN_HORIZONTAL:
|
||||||
|
self.root = HSplit([
|
||||||
|
self.active_pane,
|
||||||
|
VSplit([p for p in self.panes if p != self.active_pane])
|
||||||
|
])
|
||||||
|
|
||||||
|
# main-vertical.
|
||||||
|
elif layout_type == LayoutTypes.MAIN_VERTICAL:
|
||||||
|
self.root = VSplit([
|
||||||
|
self.active_pane,
|
||||||
|
HSplit([p for p in self.panes if p != self.active_pane])
|
||||||
|
])
|
||||||
|
|
||||||
|
# tiled.
|
||||||
|
elif layout_type == LayoutTypes.TILED:
|
||||||
|
panes = self.panes
|
||||||
|
column_count = math.ceil(len(panes) ** .5)
|
||||||
|
|
||||||
|
rows = HSplit()
|
||||||
|
current_row = VSplit()
|
||||||
|
|
||||||
|
for p in panes:
|
||||||
|
current_row.append(p)
|
||||||
|
|
||||||
|
if len(current_row) >= column_count:
|
||||||
|
rows.append(current_row)
|
||||||
|
current_row = VSplit()
|
||||||
|
if current_row:
|
||||||
|
rows.append(current_row)
|
||||||
|
|
||||||
|
self.root = rows
|
||||||
|
|
||||||
|
self.previous_selected_layout = layout_type
|
||||||
|
|
||||||
|
def select_next_layout(self, count=1):
|
||||||
|
"""
|
||||||
|
Select next layout. (Cycle through predefined layouts.)
|
||||||
|
"""
|
||||||
|
# List of all layouts. (When we have just two panes, only toggle
|
||||||
|
# between horizontal/vertical.)
|
||||||
|
if len(self.panes) == 2:
|
||||||
|
all_layouts = [LayoutTypes.EVEN_HORIZONTAL, LayoutTypes.EVEN_VERTICAL]
|
||||||
|
else:
|
||||||
|
all_layouts = LayoutTypes._ALL
|
||||||
|
|
||||||
|
# Get index of current layout.
|
||||||
|
layout = self.previous_selected_layout or LayoutTypes._ALL[-1]
|
||||||
|
try:
|
||||||
|
index = all_layouts.index(layout)
|
||||||
|
except ValueError:
|
||||||
|
index = 0
|
||||||
|
|
||||||
|
# Switch to new layout.
|
||||||
|
new_layout = all_layouts[(index + count) % len(all_layouts)]
|
||||||
|
self.select_layout(new_layout)
|
||||||
|
|
||||||
|
def select_previous_layout(self):
|
||||||
|
self.select_next_layout(count=-1)
|
||||||
|
|
||||||
|
def change_size_for_active_pane(self, up=0, right=0, down=0, left=0):
|
||||||
|
"""
|
||||||
|
Increase the size of the current pane in any of the four directions.
|
||||||
|
"""
|
||||||
|
child = self.active_pane
|
||||||
|
self.change_size_for_pane(child, up=up, right=right, down=down, left=left)
|
||||||
|
|
||||||
|
def change_size_for_pane(self, pane, up=0, right=0, down=0, left=0):
|
||||||
|
"""
|
||||||
|
Increase the size of the current pane in any of the four directions.
|
||||||
|
Positive values indicate an increase, negative values a decrease.
|
||||||
|
"""
|
||||||
|
assert isinstance(pane, Pane)
|
||||||
|
|
||||||
|
def find_split_and_child(split_cls, is_before):
|
||||||
|
" Find the split for which we will have to update the weights. "
|
||||||
|
child = pane
|
||||||
|
split = self._get_parent(child)
|
||||||
|
|
||||||
|
def found():
|
||||||
|
return isinstance(split, split_cls) and (
|
||||||
|
not is_before or split.index(child) > 0) and (
|
||||||
|
is_before or split.index(child) < len(split) - 1)
|
||||||
|
|
||||||
|
while split and not found():
|
||||||
|
child = split
|
||||||
|
split = self._get_parent(child)
|
||||||
|
|
||||||
|
return split, child # split can be None!
|
||||||
|
|
||||||
|
def handle_side(split_cls, is_before, amount, trying_other_side=False):
|
||||||
|
" Increase weights on one side. (top/left/right/bottom). "
|
||||||
|
if amount:
|
||||||
|
split, child = find_split_and_child(split_cls, is_before)
|
||||||
|
|
||||||
|
if split:
|
||||||
|
# Find neighbour.
|
||||||
|
neighbour_index = split.index(child) + (-1 if is_before else 1)
|
||||||
|
neighbour_child = split[neighbour_index]
|
||||||
|
|
||||||
|
# Increase/decrease weights.
|
||||||
|
split.weights[child] += amount
|
||||||
|
split.weights[neighbour_child] -= amount
|
||||||
|
|
||||||
|
# Ensure that all weights are at least one.
|
||||||
|
for k, value in split.weights.items():
|
||||||
|
if value < 1:
|
||||||
|
split.weights[k] = 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
# When no split has been found where we can move in this
|
||||||
|
# direction, try to move the other side instead using a
|
||||||
|
# negative amount. This happens when we run "resize-pane -R 4"
|
||||||
|
# inside the pane that is completely on the right. In that
|
||||||
|
# case it's logical to move the left border to the right
|
||||||
|
# instead.
|
||||||
|
if not trying_other_side:
|
||||||
|
handle_side(split_cls, not is_before, -amount,
|
||||||
|
trying_other_side=True)
|
||||||
|
|
||||||
|
handle_side(VSplit, True, left)
|
||||||
|
handle_side(VSplit, False, right)
|
||||||
|
handle_side(HSplit, True, up)
|
||||||
|
handle_side(HSplit, False, down)
|
||||||
|
|
||||||
|
def get_pane_index(self, pane):
|
||||||
|
" Return the index of the given pane. ValueError if not found. "
|
||||||
|
assert isinstance(pane, Pane)
|
||||||
|
return self.panes.index(pane)
|
||||||
|
|
||||||
|
|
||||||
|
class Arrangement(object):
|
||||||
|
"""
|
||||||
|
Arrangement class for one Pymux session.
|
||||||
|
This contains the list of windows and the layout of the panes for each
|
||||||
|
window. All the clients share the same Arrangement instance, but they can
|
||||||
|
have different windows active.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self.windows = []
|
||||||
|
self.base_index = 0
|
||||||
|
|
||||||
|
self._active_window_for_cli = weakref.WeakKeyDictionary()
|
||||||
|
self._prev_active_window_for_cli = weakref.WeakKeyDictionary()
|
||||||
|
|
||||||
|
# The active window of the last CLI. Used as default when a new session
|
||||||
|
# is attached.
|
||||||
|
self._last_active_window = None
|
||||||
|
|
||||||
|
def invalidation_hash(self):
|
||||||
|
"""
|
||||||
|
When this changes, the layout needs to be rebuild.
|
||||||
|
"""
|
||||||
|
if not self.windows:
|
||||||
|
return '<no-windows>'
|
||||||
|
|
||||||
|
w = self.get_active_window()
|
||||||
|
return w.invalidation_hash()
|
||||||
|
|
||||||
|
def get_active_window(self):
|
||||||
|
"""
|
||||||
|
The current active :class:`.Window`.
|
||||||
|
"""
|
||||||
|
app = get_app()
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self._active_window_for_cli[app]
|
||||||
|
except KeyError:
|
||||||
|
self._active_window_for_cli[app] = self._last_active_window or self.windows[0]
|
||||||
|
return self.windows[0]
|
||||||
|
|
||||||
|
def set_active_window(self, window):
|
||||||
|
assert isinstance(window, Window)
|
||||||
|
app = get_app()
|
||||||
|
|
||||||
|
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, pane_id):
|
||||||
|
"""
|
||||||
|
Make the window with this pane ID the active Window.
|
||||||
|
"""
|
||||||
|
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(w)
|
||||||
|
|
||||||
|
def get_previous_active_window(self):
|
||||||
|
" The previous active Window or None if unknown. "
|
||||||
|
app = get_app()
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self._prev_active_window_for_cli[app]
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_window_by_index(self, index):
|
||||||
|
" Return the Window with this index or None if not found. "
|
||||||
|
for w in self.windows:
|
||||||
|
if w.index == index:
|
||||||
|
return w
|
||||||
|
|
||||||
|
def create_window(self, pane, name=None, set_active=True):
|
||||||
|
"""
|
||||||
|
Create a new window that contains just this pane.
|
||||||
|
|
||||||
|
: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 name is None or isinstance(name, six.text_type)
|
||||||
|
|
||||||
|
# Take the first available index.
|
||||||
|
taken_indexes = [w.index for w in self.windows]
|
||||||
|
|
||||||
|
index = self.base_index
|
||||||
|
while index in taken_indexes:
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
# Create new window and add it.
|
||||||
|
w = Window(index)
|
||||||
|
w.add_pane(pane)
|
||||||
|
self.windows.append(w)
|
||||||
|
|
||||||
|
# Sort windows by index.
|
||||||
|
self.windows = sorted(self.windows, key=lambda w: w.index)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
assert w.active_pane == pane
|
||||||
|
assert w._get_parent(pane)
|
||||||
|
|
||||||
|
def move_window(self, window, new_index):
|
||||||
|
"""
|
||||||
|
Move window to a new index.
|
||||||
|
"""
|
||||||
|
assert isinstance(window, Window)
|
||||||
|
assert isinstance(new_index, int)
|
||||||
|
|
||||||
|
window.index = new_index
|
||||||
|
|
||||||
|
# Sort windows by index.
|
||||||
|
self.windows = sorted(self.windows, key=lambda w: w.index)
|
||||||
|
|
||||||
|
def get_active_pane(self):
|
||||||
|
"""
|
||||||
|
The current :class:`.Pane` from the current window.
|
||||||
|
"""
|
||||||
|
w = self.get_active_window()
|
||||||
|
if w is not None:
|
||||||
|
return w.active_pane
|
||||||
|
|
||||||
|
def remove_pane(self, pane):
|
||||||
|
"""
|
||||||
|
Remove a :class:`.Pane`. (Look in all windows.)
|
||||||
|
"""
|
||||||
|
assert isinstance(pane, Pane)
|
||||||
|
|
||||||
|
for w in self.windows:
|
||||||
|
w.remove_pane(pane)
|
||||||
|
|
||||||
|
# No panes left in this window?
|
||||||
|
if not w.has_panes:
|
||||||
|
# Focus next.
|
||||||
|
for app, active_w in self._active_window_for_cli.items():
|
||||||
|
if w == active_w:
|
||||||
|
with set_app(app):
|
||||||
|
self.focus_next_window()
|
||||||
|
|
||||||
|
self.windows.remove(w)
|
||||||
|
|
||||||
|
def focus_previous_window(self):
|
||||||
|
w = self.get_active_window()
|
||||||
|
|
||||||
|
self.set_active_window(self.windows[
|
||||||
|
(self.windows.index(w) - 1) % len(self.windows)])
|
||||||
|
|
||||||
|
def focus_next_window(self):
|
||||||
|
w = self.get_active_window()
|
||||||
|
|
||||||
|
self.set_active_window(self.windows[
|
||||||
|
(self.windows.index(w) + 1) % len(self.windows)])
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
w = self.get_active_window()
|
||||||
|
|
||||||
|
if len(w.panes) > 1:
|
||||||
|
pane = w.active_pane
|
||||||
|
self.get_active_window().remove_pane(pane)
|
||||||
|
self.create_window(pane, set_active=set_active)
|
||||||
|
|
||||||
|
def rotate_window(self, count=1):
|
||||||
|
" Rotate the panes in the active window. "
|
||||||
|
w = self.get_active_window()
|
||||||
|
w.rotate(count=count)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_panes(self):
|
||||||
|
" True when any of the windows has a :class:`.Pane`. "
|
||||||
|
for w in self.windows:
|
||||||
|
if w.has_panes:
|
||||||
|
return True
|
||||||
|
return False
|
3
pymux/client/__init__.py
Normal file
3
pymux/client/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
from .base import Client
|
||||||
|
from .defaults import create_client, list_clients
|
22
pymux/client/base.py
Normal file
22
pymux/client/base.py
Normal file
@ -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.
|
||||||
|
"""
|
24
pymux/client/defaults.py
Normal file
24
pymux/client/defaults.py
Normal file
@ -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()
|
207
pymux/client/posix.py
Normal file
207
pymux/client/posix.py
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
import getpass
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from .base import Client
|
||||||
|
|
||||||
|
INPUT_TIMEOUT = .5
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'PosixClient',
|
||||||
|
'list_clients',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PosixClient(Client):
|
||||||
|
def __init__(self, socket_name):
|
||||||
|
self.socket_name = socket_name
|
||||||
|
self._mode_context_managers = []
|
||||||
|
|
||||||
|
# Connect to socket.
|
||||||
|
self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
self.socket.connect(socket_name)
|
||||||
|
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):
|
||||||
|
"""
|
||||||
|
Ask the server to run this command.
|
||||||
|
|
||||||
|
:param pane_id: Optional identifier of the current pane.
|
||||||
|
"""
|
||||||
|
self._send_packet({
|
||||||
|
'cmd': 'run-command',
|
||||||
|
'data': command,
|
||||||
|
'pane_id': pane_id
|
||||||
|
})
|
||||||
|
|
||||||
|
def attach(self, detach_other_clients=False, color_depth=ColorDepth.DEPTH_8_BIT):
|
||||||
|
"""
|
||||||
|
Attach client user interface.
|
||||||
|
"""
|
||||||
|
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': ''
|
||||||
|
})
|
||||||
|
|
||||||
|
with raw_mode(sys.stdin.fileno()):
|
||||||
|
data_buffer = b''
|
||||||
|
|
||||||
|
stdin_fd = sys.stdin.fileno()
|
||||||
|
socket_fd = self.socket.fileno()
|
||||||
|
current_timeout = INPUT_TIMEOUT # Timeout, used to flush escape sequences.
|
||||||
|
|
||||||
|
try:
|
||||||
|
def winch_handler(signum, frame):
|
||||||
|
self._send_size()
|
||||||
|
|
||||||
|
signal.signal(signal.SIGWINCH, winch_handler)
|
||||||
|
while True:
|
||||||
|
r = select_fds([stdin_fd, socket_fd], current_timeout)
|
||||||
|
|
||||||
|
if socket_fd in r:
|
||||||
|
# Received packet from server.
|
||||||
|
#################################################################################### DECODED
|
||||||
|
try:
|
||||||
|
data = self.socket.recv(1024)
|
||||||
|
except ConnectionResetError:
|
||||||
|
pass
|
||||||
|
#################################################################################### DECODED
|
||||||
|
if data == b'':
|
||||||
|
# End of file. Connection closed.
|
||||||
|
# Reset terminal
|
||||||
|
o = Vt100_Output.from_pty(sys.stdout)
|
||||||
|
o.quit_alternate_screen()
|
||||||
|
o.disable_mouse_support()
|
||||||
|
o.disable_bracketed_paste()
|
||||||
|
o.reset_attributes()
|
||||||
|
o.flush()
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
data_buffer += data
|
||||||
|
|
||||||
|
while b'\0' in data_buffer:
|
||||||
|
pos = data_buffer.index(b'\0')
|
||||||
|
self._process(data_buffer[:pos])
|
||||||
|
data_buffer = data_buffer[pos + 1:]
|
||||||
|
|
||||||
|
elif stdin_fd in r:
|
||||||
|
# Got user input.
|
||||||
|
self._process_stdin()
|
||||||
|
current_timeout = INPUT_TIMEOUT
|
||||||
|
|
||||||
|
else:
|
||||||
|
# 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):
|
||||||
|
"""
|
||||||
|
Handle incoming packet from server.
|
||||||
|
"""
|
||||||
|
packet = json.loads(data_buffer.decode('utf-8'))
|
||||||
|
|
||||||
|
if packet['cmd'] == 'out':
|
||||||
|
# Call os.write manually. In Python2.6, sys.stdout.write doesn't use UTF-8.
|
||||||
|
os.write(sys.stdout.fileno(), packet['data'].encode('utf-8'))
|
||||||
|
|
||||||
|
elif packet['cmd'] == 'suspend':
|
||||||
|
# Suspend client process to background.
|
||||||
|
if hasattr(signal, 'SIGTSTP'):
|
||||||
|
os.kill(os.getpid(), signal.SIGTSTP)
|
||||||
|
|
||||||
|
elif packet['cmd'] == 'mode':
|
||||||
|
# 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 _process_stdin(self):
|
||||||
|
"""
|
||||||
|
Received data on stdin. Read and send to server.
|
||||||
|
"""
|
||||||
|
with nonblocking(sys.stdin.fileno()):
|
||||||
|
data = self._stdin_reader.read()
|
||||||
|
|
||||||
|
# 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):
|
||||||
|
" Report terminal size to server. "
|
||||||
|
rows, cols = _get_size(sys.stdout.fileno())
|
||||||
|
self._send_packet({
|
||||||
|
'cmd': 'size',
|
||||||
|
'data': [rows, cols]
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def list_clients():
|
||||||
|
"""
|
||||||
|
List all the servers that are running.
|
||||||
|
"""
|
||||||
|
p = '%s/pymux.sock.%s.*' % (tempfile.gettempdir(), getpass.getuser())
|
||||||
|
for path in glob.glob(p):
|
||||||
|
try:
|
||||||
|
yield PosixClient(path)
|
||||||
|
except socket.error:
|
||||||
|
pass
|
128
pymux/client/windows.py
Normal file
128
pymux/client/windows.py
Normal file
@ -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 []
|
0
pymux/commands/__init__.py
Normal file
0
pymux/commands/__init__.py
Normal file
51
pymux/commands/aliases.py
Normal file
51
pymux/commands/aliases.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
"""
|
||||||
|
Aliases for all commands.
|
||||||
|
(On purpose kept compatible with tmux.)
|
||||||
|
"""
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'ALIASES',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
ALIASES = {
|
||||||
|
'bind': 'bind-key',
|
||||||
|
'breakp': 'break-pane',
|
||||||
|
'clearhist': 'clear-history',
|
||||||
|
'confirm': 'confirm-before',
|
||||||
|
'detach': 'detach-client',
|
||||||
|
'display': 'display-message',
|
||||||
|
######################################################################################################## DECODED
|
||||||
|
'decoded': 'display-decoded',
|
||||||
|
######################################################################################################## DECODED
|
||||||
|
'displayp': 'display-panes',
|
||||||
|
'killp': 'kill-pane',
|
||||||
|
'killw': 'kill-window',
|
||||||
|
'last': 'last-window',
|
||||||
|
'lastp': 'last-pane',
|
||||||
|
'lextl': 'next-layout',
|
||||||
|
'lsk': 'list-keys',
|
||||||
|
'lsp': 'list-panes',
|
||||||
|
'movew': 'move-window',
|
||||||
|
'neww': 'new-window',
|
||||||
|
'next': 'next-window',
|
||||||
|
'pasteb': 'paste-buffer',
|
||||||
|
'prev': 'previous-window',
|
||||||
|
'prevl': 'previous-layout',
|
||||||
|
'rename': 'rename-session',
|
||||||
|
'renamew': 'rename-window',
|
||||||
|
'resizep': 'resize-pane',
|
||||||
|
'rotatew': 'rotate-window',
|
||||||
|
'selectl': 'select-layout',
|
||||||
|
'selectp': 'select-pane',
|
||||||
|
'selectw': 'select-window',
|
||||||
|
'send': 'send-keys',
|
||||||
|
'set': 'set-option',
|
||||||
|
'setw': 'set-window-option',
|
||||||
|
'source': 'source-file',
|
||||||
|
'splitw': 'split-window',
|
||||||
|
'suspendc': 'suspend-client',
|
||||||
|
'swapp': 'swap-pane',
|
||||||
|
'unbind': 'unbind-key',
|
||||||
|
}
|
676
pymux/commands/commands.py
Normal file
676
pymux/commands/commands.py
Normal file
@ -0,0 +1,676 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
import docopt
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shlex
|
||||||
|
import six
|
||||||
|
|
||||||
|
from prompt_toolkit.application.current import get_app
|
||||||
|
from prompt_toolkit.document import Document
|
||||||
|
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.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
|
||||||
|
from pymux.log import logger
|
||||||
|
from pymux.options import SetOptionError
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'call_command_handler',
|
||||||
|
'get_documentation_for_command',
|
||||||
|
'get_option_flags_for_command',
|
||||||
|
'handle_command',
|
||||||
|
'has_command_handler',
|
||||||
|
)
|
||||||
|
|
||||||
|
COMMANDS_TO_HANDLERS = {} # Global mapping of pymux commands to their handlers.
|
||||||
|
COMMANDS_TO_HELP = {}
|
||||||
|
COMMANDS_TO_OPTION_FLAGS = {}
|
||||||
|
|
||||||
|
|
||||||
|
def has_command_handler(command):
|
||||||
|
return command in COMMANDS_TO_HANDLERS
|
||||||
|
|
||||||
|
|
||||||
|
def get_documentation_for_command(command):
|
||||||
|
""" Return the help text for this command, or None if the command is not
|
||||||
|
known. """
|
||||||
|
if command in COMMANDS_TO_HELP:
|
||||||
|
return 'Usage: %s %s' % (command, COMMANDS_TO_HELP.get(command, ''))
|
||||||
|
|
||||||
|
|
||||||
|
def get_option_flags_for_command(command):
|
||||||
|
" Return a list of options (-x flags) for this command. "
|
||||||
|
return COMMANDS_TO_OPTION_FLAGS.get(command, [])
|
||||||
|
|
||||||
|
|
||||||
|
def handle_command(pymux, input_string):
|
||||||
|
"""
|
||||||
|
Handle command.
|
||||||
|
"""
|
||||||
|
assert isinstance(input_string, six.text_type)
|
||||||
|
|
||||||
|
input_string = input_string.strip()
|
||||||
|
logger.info('handle command: %s %s.', input_string, type(input_string))
|
||||||
|
|
||||||
|
if input_string and not input_string.startswith('#'): # Ignore comments.
|
||||||
|
try:
|
||||||
|
if six.PY2:
|
||||||
|
# In Python2.6, shlex doesn't work with unicode input at all.
|
||||||
|
# In Python2.7, shlex tries to encode using ASCII.
|
||||||
|
parts = shlex.split(input_string.encode('utf-8'))
|
||||||
|
parts = [p.decode('utf-8') for p in parts]
|
||||||
|
else:
|
||||||
|
parts = shlex.split(input_string)
|
||||||
|
except ValueError as e:
|
||||||
|
# E.g. missing closing quote.
|
||||||
|
pymux.show_message('Invalid command %s: %s' % (input_string, e))
|
||||||
|
else:
|
||||||
|
call_command_handler(parts[0], pymux, parts[1:])
|
||||||
|
|
||||||
|
|
||||||
|
def call_command_handler(command, pymux, arguments):
|
||||||
|
"""
|
||||||
|
Execute command.
|
||||||
|
|
||||||
|
:param arguments: List of options.
|
||||||
|
"""
|
||||||
|
assert isinstance(arguments, list)
|
||||||
|
|
||||||
|
# Resolve aliases.
|
||||||
|
command = ALIASES.get(command, command)
|
||||||
|
|
||||||
|
try:
|
||||||
|
handler = COMMANDS_TO_HANDLERS[command]
|
||||||
|
except KeyError:
|
||||||
|
pymux.show_message('Invalid command: %s' % (command,))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
handler(pymux, arguments)
|
||||||
|
except CommandException as e:
|
||||||
|
pymux.show_message(e.message)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd(name, options=''):
|
||||||
|
"""
|
||||||
|
Decorator for all commands.
|
||||||
|
|
||||||
|
Commands will receive (pymux, variables) as input.
|
||||||
|
Commands can raise CommandException.
|
||||||
|
"""
|
||||||
|
# Validate options.
|
||||||
|
if options:
|
||||||
|
try:
|
||||||
|
docopt.docopt('Usage:\n %s %s' % (name, options, ), [])
|
||||||
|
except SystemExit:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
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:
|
||||||
|
# Insert a double dash after the first non-option.
|
||||||
|
for i, p in enumerate(arguments):
|
||||||
|
if not p.startswith('-'):
|
||||||
|
arguments.insert(i + 1, '--')
|
||||||
|
break
|
||||||
|
|
||||||
|
# Parse options.
|
||||||
|
try:
|
||||||
|
# Python 2 workaround: pass bytes to docopt.
|
||||||
|
# From the following, only the bytes version returns the right
|
||||||
|
# output in Python 2:
|
||||||
|
# docopt.docopt('Usage:\n app <params>...', [b'a', b'b'])
|
||||||
|
# docopt.docopt('Usage:\n app <params>...', [u'a', u'b'])
|
||||||
|
# https://github.com/docopt/docopt/issues/30
|
||||||
|
# (Not sure how reliable this is...)
|
||||||
|
if six.PY2:
|
||||||
|
arguments = [a.encode('utf-8') for a in arguments]
|
||||||
|
|
||||||
|
received_options = docopt.docopt(
|
||||||
|
'Usage:\n %s %s' % (name, options),
|
||||||
|
arguments,
|
||||||
|
help=False) # Don't interpret the '-h' option as help.
|
||||||
|
|
||||||
|
# Make sure that all the received options from docopt are
|
||||||
|
# unicode objects. (Docopt returns 'str' for Python2.)
|
||||||
|
for k, v in received_options.items():
|
||||||
|
if isinstance(v, six.binary_type):
|
||||||
|
received_options[k] = v.decode('utf-8')
|
||||||
|
except SystemExit:
|
||||||
|
raise CommandException('Usage: %s %s' % (name, options))
|
||||||
|
|
||||||
|
# Call handler.
|
||||||
|
func(pymux, received_options)
|
||||||
|
|
||||||
|
# Invalidate all clients, not just the current CLI.
|
||||||
|
pymux.invalidate()
|
||||||
|
|
||||||
|
COMMANDS_TO_HANDLERS[name] = command_wrapper
|
||||||
|
COMMANDS_TO_HELP[name] = options
|
||||||
|
|
||||||
|
# Get list of option flags.
|
||||||
|
flags = re.findall(r'-[a-zA-Z0-9]\b', options)
|
||||||
|
COMMANDS_TO_OPTION_FLAGS[name] = flags
|
||||||
|
|
||||||
|
return func
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
class CommandException(Exception):
|
||||||
|
" When raised from a command handler, this message will be shown. "
|
||||||
|
def __init__(self, message):
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
#
|
||||||
|
# The actual commands.
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('break-pane', options='[-d]')
|
||||||
|
def break_pane(pymux, variables):
|
||||||
|
dont_focus_window = variables['-d']
|
||||||
|
|
||||||
|
pymux.arrangement.break_pane(set_active=not dont_focus_window)
|
||||||
|
pymux.invalidate()
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('select-pane', options='(-L|-R|-U|-D|-t <pane-id>)')
|
||||||
|
def select_pane(pymux, variables):
|
||||||
|
|
||||||
|
if variables['-t']:
|
||||||
|
pane_id = variables['<pane-id>']
|
||||||
|
w = pymux.arrangement.get_active_window()
|
||||||
|
|
||||||
|
if pane_id == ':.+':
|
||||||
|
w.focus_next()
|
||||||
|
elif pane_id == ':.-':
|
||||||
|
w.focus_previous()
|
||||||
|
else:
|
||||||
|
# Select pane by index.
|
||||||
|
try:
|
||||||
|
pane_id = int(pane_id[1:])
|
||||||
|
w.active_pane = w.panes[pane_id]
|
||||||
|
except (IndexError, ValueError):
|
||||||
|
raise CommandException('Invalid pane.')
|
||||||
|
|
||||||
|
else:
|
||||||
|
if variables['-L']: h = focus_left
|
||||||
|
if variables['-U']: h = focus_up
|
||||||
|
if variables['-D']: h = focus_down
|
||||||
|
if variables['-R']: h = focus_right
|
||||||
|
|
||||||
|
h(pymux)
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('select-window', options='(-t <target-window>)')
|
||||||
|
def select_window(pymux, variables):
|
||||||
|
"""
|
||||||
|
Select a window. E.g: select-window -t :3
|
||||||
|
"""
|
||||||
|
window_id = variables['<target-window>']
|
||||||
|
|
||||||
|
def invalid_window():
|
||||||
|
raise CommandException('Invalid window: %s' % window_id)
|
||||||
|
|
||||||
|
if window_id.startswith(':'):
|
||||||
|
try:
|
||||||
|
number = int(window_id[1:])
|
||||||
|
except ValueError:
|
||||||
|
invalid_window()
|
||||||
|
else:
|
||||||
|
w = pymux.arrangement.get_window_by_index(number)
|
||||||
|
if w:
|
||||||
|
pymux.arrangement.set_active_window(w)
|
||||||
|
else:
|
||||||
|
invalid_window()
|
||||||
|
else:
|
||||||
|
invalid_window()
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('move-window', options='(-t <dst-window>)')
|
||||||
|
def move_window(pymux, variables):
|
||||||
|
"""
|
||||||
|
Move window to a new index.
|
||||||
|
"""
|
||||||
|
dst_window = variables['<dst-window>']
|
||||||
|
try:
|
||||||
|
new_index = int(dst_window)
|
||||||
|
except ValueError:
|
||||||
|
raise CommandException('Invalid window index: %r' % (dst_window, ))
|
||||||
|
|
||||||
|
# Check first whether the index was not yet taken.
|
||||||
|
if pymux.arrangement.get_window_by_index(new_index):
|
||||||
|
raise CommandException("Can't move window: index in use.")
|
||||||
|
|
||||||
|
# Save index.
|
||||||
|
w = pymux.arrangement.get_active_window()
|
||||||
|
pymux.arrangement.move_window(w, new_index)
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('rotate-window', options='[-D|-U]')
|
||||||
|
def rotate_window(pymux, variables):
|
||||||
|
if variables['-D']:
|
||||||
|
pymux.arrangement.rotate_window(count=-1)
|
||||||
|
else:
|
||||||
|
pymux.arrangement.rotate_window()
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('swap-pane', options='(-D|-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, variables):
|
||||||
|
pane = pymux.arrangement.get_active_pane()
|
||||||
|
pymux.kill_pane(pane)
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('kill-window')
|
||||||
|
def kill_window(pymux, variables):
|
||||||
|
" Kill all panes in the current window. "
|
||||||
|
for pane in pymux.arrangement.get_active_window().panes:
|
||||||
|
pymux.kill_pane(pane)
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('suspend-client')
|
||||||
|
def suspend_client(pymux, variables):
|
||||||
|
connection = pymux.get_connection()
|
||||||
|
|
||||||
|
if connection:
|
||||||
|
connection.suspend_client_to_background()
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('clock-mode')
|
||||||
|
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, variables):
|
||||||
|
w = pymux.arrangement.get_active_window()
|
||||||
|
prev_active_pane = w.previous_active_pane
|
||||||
|
|
||||||
|
if prev_active_pane:
|
||||||
|
w.active_pane = prev_active_pane
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('next-layout')
|
||||||
|
def next_layout(pymux, variables):
|
||||||
|
" Select next layout. "
|
||||||
|
pane = pymux.arrangement.get_active_window()
|
||||||
|
if pane:
|
||||||
|
pane.select_next_layout()
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('previous-layout')
|
||||||
|
def previous_layout(pymux, variables):
|
||||||
|
" Select previous layout. "
|
||||||
|
pane = pymux.arrangement.get_active_window()
|
||||||
|
if pane:
|
||||||
|
pane.select_previous_layout()
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('new-window', options='[(-n <name>)] [(-c <start-directory>)] [<executable>]')
|
||||||
|
def new_window(pymux, variables):
|
||||||
|
executable = variables['<executable>']
|
||||||
|
start_directory = variables['<start-directory>']
|
||||||
|
name = variables['<name>']
|
||||||
|
|
||||||
|
pymux.create_window(executable, start_directory=start_directory, name=name)
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('next-window')
|
||||||
|
def next_window(pymux, variables):
|
||||||
|
" Focus the next window. "
|
||||||
|
pymux.arrangement.focus_next_window()
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('last-window')
|
||||||
|
def _(pymux, variables):
|
||||||
|
" Go to previous active window. "
|
||||||
|
w = pymux.arrangement.get_previous_active_window()
|
||||||
|
|
||||||
|
if w:
|
||||||
|
pymux.arrangement.set_active_window(w)
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('previous-window')
|
||||||
|
def previous_window(pymux, variables):
|
||||||
|
" Focus the previous window. "
|
||||||
|
pymux.arrangement.focus_previous_window()
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('select-layout', options='<layout-type>')
|
||||||
|
def select_layout(pymux, variables):
|
||||||
|
layout_type = variables['<layout-type>']
|
||||||
|
|
||||||
|
if layout_type in LayoutTypes._ALL:
|
||||||
|
pymux.arrangement.get_active_window().select_layout(layout_type)
|
||||||
|
else:
|
||||||
|
raise CommandException('Invalid layout type.')
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('rename-window', options='<name>')
|
||||||
|
def rename_window(pymux, variables):
|
||||||
|
"""
|
||||||
|
Rename the active window.
|
||||||
|
"""
|
||||||
|
pymux.arrangement.get_active_window().chosen_name = variables['<name>']
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('rename-pane', options='<name>')
|
||||||
|
def rename_pane(pymux, variables):
|
||||||
|
"""
|
||||||
|
Rename the active pane.
|
||||||
|
"""
|
||||||
|
pymux.arrangement.get_active_pane().chosen_name = variables['<name>']
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('rename-session', options='<name>')
|
||||||
|
def rename_session(pymux, variables):
|
||||||
|
"""
|
||||||
|
Rename this session.
|
||||||
|
"""
|
||||||
|
pymux.session_name = variables['<name>']
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('split-window', options='[-v|-h] [(-c <start-directory>)] [<executable>]')
|
||||||
|
def split_window(pymux, variables):
|
||||||
|
"""
|
||||||
|
Split horizontally or vertically.
|
||||||
|
"""
|
||||||
|
executable = variables['<executable>']
|
||||||
|
start_directory = variables['<start-directory>']
|
||||||
|
|
||||||
|
# The tmux definition of horizontal is the opposite of prompt_toolkit.
|
||||||
|
pymux.add_process(executable, vsplit=variables['-h'],
|
||||||
|
start_directory=start_directory)
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('resize-pane', options="[(-L <left>)] [(-U <up>)] [(-D <down>)] [(-R <right>)] [-Z]")
|
||||||
|
def resize_pane(pymux, variables):
|
||||||
|
"""
|
||||||
|
Resize/zoom the active pane.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
left = int(variables['<left>'] or 0)
|
||||||
|
right = int(variables['<right>'] or 0)
|
||||||
|
up = int(variables['<up>'] or 0)
|
||||||
|
down = int(variables['<down>'] or 0)
|
||||||
|
except ValueError:
|
||||||
|
raise CommandException('Expecting an integer.')
|
||||||
|
|
||||||
|
w = pymux.arrangement.get_active_window()
|
||||||
|
|
||||||
|
if w:
|
||||||
|
w.change_size_for_active_pane(up=up, right=right, down=down, left=left)
|
||||||
|
|
||||||
|
# Zoom in/out.
|
||||||
|
if variables['-Z']:
|
||||||
|
w.zoom = not w.zoom
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('detach-client')
|
||||||
|
def detach_client(pymux, variables):
|
||||||
|
"""
|
||||||
|
Detach client.
|
||||||
|
"""
|
||||||
|
pymux.detach_client(get_app())
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('confirm-before', options='[(-p <message>)] <command>')
|
||||||
|
def confirm_before(pymux, variables):
|
||||||
|
client_state = pymux.get_client_state()
|
||||||
|
|
||||||
|
client_state.confirm_text = variables['<message>'] or ''
|
||||||
|
client_state.confirm_command = variables['<command>']
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('command-prompt', options='[(-p <message>)] [(-I <default>)] [<command>]')
|
||||||
|
def command_prompt(pymux, variables):
|
||||||
|
"""
|
||||||
|
Enter command prompt.
|
||||||
|
"""
|
||||||
|
client_state = pymux.get_client_state()
|
||||||
|
|
||||||
|
if variables['<command>']:
|
||||||
|
# When a 'command' has been given.
|
||||||
|
client_state.prompt_text = variables['<message>'] or '(%s)' % variables['<command>'].split()[0]
|
||||||
|
client_state.prompt_command = variables['<command>']
|
||||||
|
|
||||||
|
client_state.prompt_mode = True
|
||||||
|
client_state.prompt_buffer.reset(Document(
|
||||||
|
format_pymux_string(pymux, variables['<default>'] or '')))
|
||||||
|
|
||||||
|
get_app().layout.focus(client_state.prompt_buffer)
|
||||||
|
else:
|
||||||
|
# Show the ':' prompt.
|
||||||
|
client_state.prompt_text = ''
|
||||||
|
client_state.prompt_command = ''
|
||||||
|
|
||||||
|
get_app().layout.focus(client_state.command_buffer)
|
||||||
|
|
||||||
|
# Go to insert mode.
|
||||||
|
get_app().vi_state.input_mode = InputMode.INSERT
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('send-prefix')
|
||||||
|
def send_prefix(pymux, variables):
|
||||||
|
"""
|
||||||
|
Send prefix to active pane.
|
||||||
|
"""
|
||||||
|
process = pymux.arrangement.get_active_pane().process
|
||||||
|
|
||||||
|
for k in pymux.key_bindings_manager.prefix:
|
||||||
|
vt100_data = prompt_toolkit_key_to_vt100_key(k)
|
||||||
|
process.write_input(vt100_data)
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('bind-key', options='[-n] <key> [--] <command> [<arguments>...]')
|
||||||
|
def bind_key(pymux, variables):
|
||||||
|
"""
|
||||||
|
Bind a key sequence.
|
||||||
|
-n: Not necessary to use the prefix.
|
||||||
|
"""
|
||||||
|
key = variables['<key>']
|
||||||
|
command = variables['<command>']
|
||||||
|
arguments = variables['<arguments>']
|
||||||
|
needs_prefix = not variables['-n']
|
||||||
|
|
||||||
|
try:
|
||||||
|
pymux.key_bindings_manager.add_custom_binding(
|
||||||
|
key, command, arguments, needs_prefix=needs_prefix)
|
||||||
|
except ValueError:
|
||||||
|
raise CommandException('Invalid key: %r' % (key, ))
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('unbind-key', options='[-n] <key>')
|
||||||
|
def unbind_key(pymux, variables):
|
||||||
|
"""
|
||||||
|
Remove key binding.
|
||||||
|
"""
|
||||||
|
key = variables['<key>']
|
||||||
|
needs_prefix = not variables['-n']
|
||||||
|
|
||||||
|
pymux.key_bindings_manager.remove_custom_binding(
|
||||||
|
key, needs_prefix=needs_prefix)
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('send-keys', options='<keys>...')
|
||||||
|
def send_keys(pymux, variables):
|
||||||
|
"""
|
||||||
|
Send key strokes to the active process.
|
||||||
|
"""
|
||||||
|
pane = pymux.arrangement.get_active_pane()
|
||||||
|
|
||||||
|
if pane.display_scroll_buffer:
|
||||||
|
raise CommandException('Cannot send keys. Pane is in copy mode.')
|
||||||
|
|
||||||
|
for key in variables['<keys>']:
|
||||||
|
# Translate key from pymux key to prompt_toolkit key.
|
||||||
|
try:
|
||||||
|
keys_sequence = pymux_key_to_prompt_toolkit_key_sequence(key)
|
||||||
|
except ValueError:
|
||||||
|
raise CommandException('Invalid key: %r' % (key, ))
|
||||||
|
|
||||||
|
# Translate prompt_toolkit key to VT100 key.
|
||||||
|
for k in keys_sequence:
|
||||||
|
pane.process.write_key(k)
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('copy-mode', options='[-u]')
|
||||||
|
def copy_mode(pymux, variables):
|
||||||
|
"""
|
||||||
|
Enter copy mode.
|
||||||
|
"""
|
||||||
|
go_up = variables['-u'] # Go in copy mode and page-up directly.
|
||||||
|
# TODO: handle '-u'
|
||||||
|
|
||||||
|
pane = pymux.arrangement.get_active_pane()
|
||||||
|
pane.enter_copy_mode()
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('paste-buffer')
|
||||||
|
def paste_buffer(pymux, variables):
|
||||||
|
"""
|
||||||
|
Paste clipboard content into buffer.
|
||||||
|
"""
|
||||||
|
pane = pymux.arrangement.get_active_pane()
|
||||||
|
pane.process.write_input(get_app().clipboard.get_data().text, paste=True)
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('source-file', options='<filename>')
|
||||||
|
def source_file(pymux, variables):
|
||||||
|
"""
|
||||||
|
Source configuration file.
|
||||||
|
"""
|
||||||
|
filename = os.path.expanduser(variables['<filename>'])
|
||||||
|
try:
|
||||||
|
with open(filename, 'rb') as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.decode('utf-8')
|
||||||
|
handle_command(pymux, line)
|
||||||
|
except IOError as e:
|
||||||
|
raise CommandException('IOError: %s' % (e, ))
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('set-option', options='<option> <value>')
|
||||||
|
def set_option(pymux, variables, window=False):
|
||||||
|
name = variables['<option>']
|
||||||
|
value = variables['<value>']
|
||||||
|
|
||||||
|
if window:
|
||||||
|
option = pymux.window_options.get(name)
|
||||||
|
else:
|
||||||
|
option = pymux.options.get(name)
|
||||||
|
|
||||||
|
if option:
|
||||||
|
try:
|
||||||
|
option.set_value(pymux, value)
|
||||||
|
except SetOptionError as e:
|
||||||
|
raise CommandException(e.message)
|
||||||
|
else:
|
||||||
|
raise CommandException('Invalid option: %s' % (name, ))
|
||||||
|
|
||||||
|
@cmd('set-window-option', options='<option> <value>')
|
||||||
|
def set_window_option(pymux, variables):
|
||||||
|
set_option(pymux, variables, window=True)
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('display-panes')
|
||||||
|
def display_panes(pymux, variables):
|
||||||
|
" Display the pane numbers. "
|
||||||
|
pymux.display_pane_numbers = True
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('display-message', options='<message>')
|
||||||
|
def display_message(pymux, variables):
|
||||||
|
" Display a message. "
|
||||||
|
message = variables['<message>']
|
||||||
|
client_state = pymux.get_client_state()
|
||||||
|
client_state.message = message
|
||||||
|
|
||||||
|
############################################################################################################ DECODED
|
||||||
|
@cmd('display-decoded', options='<decoded>')
|
||||||
|
def display_decoded(pymux, variables):
|
||||||
|
" Display a decoded. "
|
||||||
|
decoded = variables['<decoded>']
|
||||||
|
client_state = pymux.get_client_state()
|
||||||
|
client_state.message = decoded
|
||||||
|
############################################################################################################ DECODED
|
||||||
|
|
||||||
|
@cmd('clear-history')
|
||||||
|
def clear_history(pymux, variables):
|
||||||
|
" Clear scrollback buffer. "
|
||||||
|
pane = pymux.arrangement.get_active_pane()
|
||||||
|
|
||||||
|
if pane.display_scroll_buffer:
|
||||||
|
raise CommandException('Not available in copy mode')
|
||||||
|
else:
|
||||||
|
pane.process.screen.clear_history()
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('list-keys')
|
||||||
|
def list_keys(pymux, variables):
|
||||||
|
"""
|
||||||
|
Display all configured key bindings.
|
||||||
|
"""
|
||||||
|
# Create help string.
|
||||||
|
result = []
|
||||||
|
|
||||||
|
for k, custom_binding in pymux.key_bindings_manager.custom_bindings.items():
|
||||||
|
needs_prefix, key = k
|
||||||
|
|
||||||
|
result.append('bind-key %3s %-10s %s %s' % (
|
||||||
|
('-n' if needs_prefix else ''), key, custom_binding.command,
|
||||||
|
' '.join(map(wrap_argument, custom_binding.arguments))))
|
||||||
|
|
||||||
|
# Display help in pane.
|
||||||
|
result = '\n'.join(sorted(result))
|
||||||
|
pymux.get_client_state().layout_manager.display_popup('list-keys', result)
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('list-panes')
|
||||||
|
def list_panes(pymux, variables):
|
||||||
|
"""
|
||||||
|
Display a list of all the panes.
|
||||||
|
"""
|
||||||
|
w = pymux.arrangement.get_active_window()
|
||||||
|
active_pane = w.active_pane
|
||||||
|
|
||||||
|
result = []
|
||||||
|
|
||||||
|
for i, p in enumerate(w.panes):
|
||||||
|
process = p.process
|
||||||
|
|
||||||
|
result.append('%i: [%sx%s] [history %s/%s] %s' % (
|
||||||
|
i, process.sx, process.sy,
|
||||||
|
min(pymux.history_limit, process.screen.line_offset + process.sy),
|
||||||
|
pymux.history_limit,
|
||||||
|
('(active)' if p == active_pane else '')))
|
||||||
|
|
||||||
|
# Display help in pane.
|
||||||
|
result = '\n'.join(sorted(result))
|
||||||
|
pymux.get_client_state().layout_manager.display_popup('list-keys', result)
|
||||||
|
|
||||||
|
|
||||||
|
@cmd('show-buffer')
|
||||||
|
def show_buffer(pymux, variables):
|
||||||
|
"""
|
||||||
|
Display the clipboard content.
|
||||||
|
"""
|
||||||
|
text = get_app().clipboard.get_data().text
|
||||||
|
pymux.get_client_state().layout_manager.display_popup('show-buffer', text)
|
||||||
|
|
||||||
|
|
||||||
|
# Check whether all aliases point to real commands.
|
||||||
|
for k in ALIASES.values():
|
||||||
|
assert k in COMMANDS_TO_HANDLERS
|
185
pymux/commands/completer.py
Normal file
185
pymux/commands/completer.py
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from prompt_toolkit.completion import Completer, Completion, WordCompleter
|
||||||
|
from prompt_toolkit.document import Document
|
||||||
|
|
||||||
|
from .aliases import ALIASES
|
||||||
|
from .commands import COMMANDS_TO_HANDLERS, get_option_flags_for_command
|
||||||
|
from .utils import wrap_argument
|
||||||
|
|
||||||
|
from pymux.arrangement import LayoutTypes
|
||||||
|
from pymux.key_mappings import PYMUX_TO_PROMPT_TOOLKIT_KEYS
|
||||||
|
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'create_command_completer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_command_completer(pymux):
|
||||||
|
return ShlexCompleter(partial(get_completions_for_parts, pymux=pymux))
|
||||||
|
|
||||||
|
|
||||||
|
class CommandCompleter(Completer):
|
||||||
|
"""
|
||||||
|
Completer for command names.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
# Completer for full command names.
|
||||||
|
self._command_completer = WordCompleter(
|
||||||
|
sorted(COMMANDS_TO_HANDLERS.keys()),
|
||||||
|
ignore_case=True, WORD=True, match_middle=True)
|
||||||
|
|
||||||
|
# Completer for aliases.
|
||||||
|
self._aliases_completer = WordCompleter(
|
||||||
|
sorted(ALIASES.keys()),
|
||||||
|
ignore_case=True, WORD=True, match_middle=True)
|
||||||
|
|
||||||
|
def get_completions(self, document, complete_event):
|
||||||
|
# First, complete on full command names.
|
||||||
|
found = False
|
||||||
|
|
||||||
|
for c in self._command_completer.get_completions(document, complete_event):
|
||||||
|
found = True
|
||||||
|
yield c
|
||||||
|
|
||||||
|
# When no matches are found, complete aliases instead.
|
||||||
|
# The completion however, inserts the full name.
|
||||||
|
if not found:
|
||||||
|
for c in self._aliases_completer.get_completions(document, complete_event):
|
||||||
|
full_name = ALIASES.get(c.display)
|
||||||
|
|
||||||
|
yield Completion(full_name,
|
||||||
|
start_position=c.start_position,
|
||||||
|
display='%s (%s)' % (c.display, full_name))
|
||||||
|
|
||||||
|
|
||||||
|
_command_completer = CommandCompleter()
|
||||||
|
_layout_type_completer = WordCompleter(sorted(LayoutTypes._ALL), WORD=True)
|
||||||
|
_keys_completer = WordCompleter(sorted(PYMUX_TO_PROMPT_TOOLKIT_KEYS.keys()),
|
||||||
|
ignore_case=True, WORD=True)
|
||||||
|
|
||||||
|
|
||||||
|
def get_completions_for_parts(parts, last_part, complete_event, pymux):
|
||||||
|
completer = None
|
||||||
|
|
||||||
|
# Resolve aliases.
|
||||||
|
if len(parts) > 0:
|
||||||
|
parts = [ALIASES.get(parts[0], parts[0])] + parts[1:]
|
||||||
|
|
||||||
|
if len(parts) == 0:
|
||||||
|
# New command.
|
||||||
|
completer = _command_completer
|
||||||
|
|
||||||
|
elif len(parts) >= 1 and last_part.startswith('-'):
|
||||||
|
flags = get_option_flags_for_command(parts[0])
|
||||||
|
completer = WordCompleter(sorted(flags), WORD=True)
|
||||||
|
|
||||||
|
elif len(parts) == 1 and parts[0] in ('set-option', 'set-window-option'):
|
||||||
|
options = pymux.options if parts[0] == 'set-option' else pymux.window_options
|
||||||
|
|
||||||
|
completer = WordCompleter(sorted(options.keys()), sentence=True)
|
||||||
|
|
||||||
|
elif len(parts) == 2 and parts[0] in ('set-option', 'set-window-option'):
|
||||||
|
options = pymux.options if parts[0] == 'set-option' else pymux.window_options
|
||||||
|
|
||||||
|
option = options.get(parts[1])
|
||||||
|
if option:
|
||||||
|
completer = WordCompleter(sorted(option.get_all_values(pymux)), sentence=True)
|
||||||
|
|
||||||
|
elif len(parts) == 1 and parts[0] == 'select-layout':
|
||||||
|
completer = _layout_type_completer
|
||||||
|
|
||||||
|
elif len(parts) == 1 and parts[0] == 'send-keys':
|
||||||
|
completer = _keys_completer
|
||||||
|
|
||||||
|
elif parts[0] == 'bind-key':
|
||||||
|
if len(parts) == 1:
|
||||||
|
completer = _keys_completer
|
||||||
|
|
||||||
|
elif len(parts) == 2:
|
||||||
|
completer = _command_completer
|
||||||
|
|
||||||
|
# Recursive, for bind-key options.
|
||||||
|
if parts and parts[0] == 'bind-key' and len(parts) > 2:
|
||||||
|
for c in get_completions_for_parts(parts[2:], last_part, complete_event, pymux):
|
||||||
|
yield c
|
||||||
|
|
||||||
|
if completer:
|
||||||
|
for c in completer.get_completions(Document(last_part), complete_event):
|
||||||
|
yield c
|
||||||
|
|
||||||
|
|
||||||
|
class ShlexCompleter(Completer):
|
||||||
|
"""
|
||||||
|
Completer that can be used when the input is parsed with shlex.
|
||||||
|
"""
|
||||||
|
def __init__(self, get_completions_for_parts):
|
||||||
|
assert callable(get_completions_for_parts)
|
||||||
|
self.get_completions_for_parts = get_completions_for_parts
|
||||||
|
|
||||||
|
def get_completions(self, document, complete_event):
|
||||||
|
text = document.text_before_cursor
|
||||||
|
|
||||||
|
parts, part_start_pos = self.parse(text)
|
||||||
|
|
||||||
|
for c in self.get_completions_for_parts(parts[:-1], parts[-1], complete_event):
|
||||||
|
yield Completion(wrap_argument(parts[-1][:c.start_position] + c.text),
|
||||||
|
start_position=part_start_pos - len(document.text),
|
||||||
|
display=c.display,
|
||||||
|
display_meta=c.display_meta)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse(cls, text):
|
||||||
|
"""
|
||||||
|
Parse the given text. Returns a tuple:
|
||||||
|
(list_of_parts, start_pos_of_the_last_part).
|
||||||
|
"""
|
||||||
|
OUTSIDE, IN_DOUBLE, IN_SINGLE = 0, 1, 2
|
||||||
|
|
||||||
|
iterator = enumerate(text)
|
||||||
|
state = OUTSIDE
|
||||||
|
parts = []
|
||||||
|
current_part = ''
|
||||||
|
part_start_pos = 0
|
||||||
|
|
||||||
|
for i, c in iterator: # XXX: correctly handle empty strings.
|
||||||
|
if state == OUTSIDE:
|
||||||
|
if c.isspace():
|
||||||
|
# New part.
|
||||||
|
if current_part:
|
||||||
|
parts.append(current_part)
|
||||||
|
part_start_pos = i + 1
|
||||||
|
current_part = ''
|
||||||
|
elif c == '"':
|
||||||
|
state = IN_DOUBLE
|
||||||
|
elif c == "'":
|
||||||
|
state = IN_SINGLE
|
||||||
|
else:
|
||||||
|
current_part += c
|
||||||
|
|
||||||
|
elif state == IN_SINGLE:
|
||||||
|
if c == "'":
|
||||||
|
state = OUTSIDE
|
||||||
|
elif c == "\\":
|
||||||
|
next(iterator)
|
||||||
|
current_part += c
|
||||||
|
else:
|
||||||
|
current_part += c
|
||||||
|
|
||||||
|
elif state == IN_DOUBLE:
|
||||||
|
if c == '"':
|
||||||
|
state = OUTSIDE
|
||||||
|
elif c == "\\":
|
||||||
|
next(iterator)
|
||||||
|
current_part += c
|
||||||
|
else:
|
||||||
|
current_part += c
|
||||||
|
|
||||||
|
parts.append(current_part)
|
||||||
|
return parts, part_start_pos
|
||||||
|
|
||||||
|
|
||||||
|
# assert ShlexCompleter.parse('"hello" world') == (['hello', 'world'], 8)
|
15
pymux/commands/utils.py
Normal file
15
pymux/commands/utils.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'wrap_argument',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_argument(text):
|
||||||
|
"""
|
||||||
|
Wrap command argument in quotes and escape when this contains special characters.
|
||||||
|
"""
|
||||||
|
if not any(x in text for x in [' ', '"', "'", '\\']):
|
||||||
|
return text
|
||||||
|
else:
|
||||||
|
return '"%s"' % (text.replace('\\', r'\\').replace('"', r'\"'), )
|
0
pymux/entry_points/__init__.py
Normal file
0
pymux/entry_points/__init__.py
Normal file
163
pymux/entry_points/run_pymux.py
Normal file
163
pymux/entry_points/run_pymux.py
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
pymux: Pure Python terminal multiplexer.
|
||||||
|
Usage:
|
||||||
|
pymux [(standalone|start-server|attach)] [-d]
|
||||||
|
[--truecolor] [--ansicolor] [(-S <socket>)] [(-f <file>)]
|
||||||
|
[(--log <logfile>)]
|
||||||
|
[--] [<command>]
|
||||||
|
pymux list-sessions
|
||||||
|
pymux -h | --help
|
||||||
|
pymux <command>
|
||||||
|
|
||||||
|
Options:
|
||||||
|
standalone : Run as a standalone process. (for debugging, detaching is
|
||||||
|
not possible.
|
||||||
|
start-server : Run a server daemon that can be attached later on.
|
||||||
|
attach : Attach to a running session.
|
||||||
|
|
||||||
|
-f : Path to configuration file. By default: '~/.pymux.conf'.
|
||||||
|
-S : Unix socket path.
|
||||||
|
-d : Detach all other clients, when attaching.
|
||||||
|
--log : Logfile.
|
||||||
|
--truecolor : Render true color (24 bit) instead of 256 colors.
|
||||||
|
(Each client can set this separately.)
|
||||||
|
"""
|
||||||
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
|
from prompt_toolkit.output import ColorDepth
|
||||||
|
from pymux.main import Pymux
|
||||||
|
from pymux.client import create_client, list_clients
|
||||||
|
from pymux.utils import daemonize
|
||||||
|
|
||||||
|
import docopt
|
||||||
|
import getpass
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'run',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
a = docopt.docopt(__doc__)
|
||||||
|
socket_name = a['<socket>'] or os.environ.get('PYMUX')
|
||||||
|
socket_name_from_env = not a['<socket>'] and os.environ.get('PYMUX')
|
||||||
|
filename = a['<file>']
|
||||||
|
command = a['<command>']
|
||||||
|
true_color = a['--truecolor']
|
||||||
|
ansi_colors_only = a['--ansicolor'] or \
|
||||||
|
bool(os.environ.get('PROMPT_TOOLKIT_ANSI_COLORS_ONLY', False))
|
||||||
|
|
||||||
|
# Parse pane_id from socket_name. It looks like "socket_name,pane_id".
|
||||||
|
if socket_name and ',' in socket_name:
|
||||||
|
socket_name, pane_id = socket_name.rsplit(',', 1)
|
||||||
|
else:
|
||||||
|
pane_id = None
|
||||||
|
|
||||||
|
# Color depth.
|
||||||
|
if ansi_colors_only:
|
||||||
|
color_depth = ColorDepth.DEPTH_4_BIT
|
||||||
|
elif true_color:
|
||||||
|
color_depth = ColorDepth.DEPTH_24_BIT
|
||||||
|
else:
|
||||||
|
color_depth = ColorDepth.DEPTH_8_BIT
|
||||||
|
|
||||||
|
# Expand socket name. (Make it possible to just accept numbers.)
|
||||||
|
if socket_name and socket_name.isdigit():
|
||||||
|
socket_name = '%s/pymux.sock.%s.%s' % (
|
||||||
|
tempfile.gettempdir(), getpass.getuser(), socket_name)
|
||||||
|
|
||||||
|
# Configuration filename.
|
||||||
|
default_config = os.path.abspath(os.path.expanduser('~/.pymux.conf'))
|
||||||
|
if not filename and os.path.exists(default_config):
|
||||||
|
filename = default_config
|
||||||
|
|
||||||
|
if filename:
|
||||||
|
filename = os.path.abspath(os.path.expanduser(filename))
|
||||||
|
|
||||||
|
# Create 'Pymux'.
|
||||||
|
mux = Pymux(source_file=filename, startup_command=command)
|
||||||
|
|
||||||
|
# Setup logging.
|
||||||
|
if a['<logfile>']:
|
||||||
|
logging.basicConfig(filename=a['<logfile>'], level=logging.DEBUG)
|
||||||
|
|
||||||
|
if a['standalone']:
|
||||||
|
mux.run_standalone(color_depth=color_depth)
|
||||||
|
|
||||||
|
elif a['list-sessions'] or a['<command>'] in ('ls', 'list-sessions'):
|
||||||
|
for c in list_clients():
|
||||||
|
print(c.socket_name)
|
||||||
|
|
||||||
|
elif a['start-server']:
|
||||||
|
if socket_name_from_env:
|
||||||
|
_socket_from_env_warning()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Log to stdout.
|
||||||
|
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
|
||||||
|
|
||||||
|
# Run server.
|
||||||
|
socket_name = mux.listen_on_socket()
|
||||||
|
try:
|
||||||
|
mux.run_server()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
elif a['attach']:
|
||||||
|
if socket_name_from_env:
|
||||||
|
_socket_from_env_warning()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
detach_other_clients = a['-d']
|
||||||
|
|
||||||
|
if socket_name:
|
||||||
|
create_client(socket_name).attach(
|
||||||
|
detach_other_clients=detach_other_clients,
|
||||||
|
color_depth=color_depth)
|
||||||
|
else:
|
||||||
|
# Connect to the first server.
|
||||||
|
for c in list_clients():
|
||||||
|
c.attach(detach_other_clients=detach_other_clients,
|
||||||
|
color_depth=color_depth)
|
||||||
|
break
|
||||||
|
else: # Nobreak.
|
||||||
|
print('No pymux instance found.')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
elif a['<command>'] and socket_name:
|
||||||
|
create_client(socket_name).run_command(a['<command>'], pane_id)
|
||||||
|
|
||||||
|
elif not socket_name:
|
||||||
|
# Run client/server combination.
|
||||||
|
socket_name = mux.listen_on_socket(socket_name)
|
||||||
|
pid = daemonize()
|
||||||
|
|
||||||
|
if pid > 0:
|
||||||
|
# Create window. It is important that this happens in the daemon,
|
||||||
|
# because the parent of the process running inside should be this
|
||||||
|
# daemon. (Otherwise the `waitpid` call won't work.)
|
||||||
|
mux.run_server()
|
||||||
|
else:
|
||||||
|
create_client(socket_name).attach(color_depth=color_depth)
|
||||||
|
|
||||||
|
else:
|
||||||
|
if socket_name_from_env:
|
||||||
|
_socket_from_env_warning()
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print('Invalid command.')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def _socket_from_env_warning():
|
||||||
|
print('Please be careful nesting pymux sessions.')
|
||||||
|
print('Unset PYMUX environment variable first.')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run()
|
13
pymux/enums.py
Normal file
13
pymux/enums.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'COMMAND',
|
||||||
|
'PROMPT',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#: Name of the command buffer.
|
||||||
|
COMMAND = 'COMMAND'
|
||||||
|
|
||||||
|
#: Name of the input for a "command-prompt" command.
|
||||||
|
PROMPT = 'PROMPT'
|
101
pymux/filters.py
Normal file
101
pymux/filters.py
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
from prompt_toolkit.filters import Filter
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'HasPrefix',
|
||||||
|
'WaitsForConfirmation',
|
||||||
|
'InCommandMode',
|
||||||
|
'WaitsForPrompt',
|
||||||
|
'InScrollBuffer',
|
||||||
|
'InScrollBufferNotSearching',
|
||||||
|
'InScrollBufferSearching',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HasPrefix(Filter):
|
||||||
|
"""
|
||||||
|
When the prefix key (Usual C-b) has been pressed.
|
||||||
|
"""
|
||||||
|
def __init__(self, pymux):
|
||||||
|
self.pymux = pymux
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
return self.pymux.get_client_state().has_prefix
|
||||||
|
|
||||||
|
|
||||||
|
class WaitsForConfirmation(Filter):
|
||||||
|
"""
|
||||||
|
Waiting for a yes/no key press.
|
||||||
|
"""
|
||||||
|
def __init__(self, pymux):
|
||||||
|
self.pymux = pymux
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
return bool(self.pymux.get_client_state().confirm_command)
|
||||||
|
|
||||||
|
|
||||||
|
class InCommandMode(Filter):
|
||||||
|
"""
|
||||||
|
When ':' has been pressed.'
|
||||||
|
"""
|
||||||
|
def __init__(self, pymux):
|
||||||
|
self.pymux = pymux
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
client_state = self.pymux.get_client_state()
|
||||||
|
return client_state.command_mode and not client_state.confirm_command
|
||||||
|
|
||||||
|
|
||||||
|
class WaitsForPrompt(Filter):
|
||||||
|
"""
|
||||||
|
Waiting for input for a "command-prompt" command.
|
||||||
|
"""
|
||||||
|
def __init__(self, pymux):
|
||||||
|
self.pymux = pymux
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
client_state = self.pymux.get_client_state()
|
||||||
|
return bool(client_state.prompt_command) and not client_state.confirm_command
|
||||||
|
|
||||||
|
|
||||||
|
def _confirm_or_prompt_or_command(pymux):
|
||||||
|
" True when we are waiting for a command, prompt or confirmation. "
|
||||||
|
client_state = pymux.get_client_state()
|
||||||
|
if client_state.confirm_text or client_state.prompt_command or client_state.command_mode:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class InScrollBuffer(Filter):
|
||||||
|
def __init__(self, pymux):
|
||||||
|
self.pymux = pymux
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
if _confirm_or_prompt_or_command(self.pymux):
|
||||||
|
return False
|
||||||
|
|
||||||
|
pane = self.pymux.arrangement.get_active_pane()
|
||||||
|
return pane.display_scroll_buffer
|
||||||
|
|
||||||
|
|
||||||
|
class InScrollBufferNotSearching(Filter):
|
||||||
|
def __init__(self, pymux):
|
||||||
|
self.pymux = pymux
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
if _confirm_or_prompt_or_command(self.pymux):
|
||||||
|
return False
|
||||||
|
|
||||||
|
pane = self.pymux.arrangement.get_active_pane()
|
||||||
|
return pane.display_scroll_buffer and not pane.is_searching
|
||||||
|
|
||||||
|
|
||||||
|
class InScrollBufferSearching(Filter):
|
||||||
|
def __init__(self, pymux):
|
||||||
|
self.pymux = pymux
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
if _confirm_or_prompt_or_command(self.pymux):
|
||||||
|
return False
|
||||||
|
|
||||||
|
pane = self.pymux.arrangement.get_active_pane()
|
||||||
|
return pane.display_scroll_buffer and pane.is_searching
|
99
pymux/format.py
Normal file
99
pymux/format.py
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
"""
|
||||||
|
Pymux string formatting.
|
||||||
|
"""
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
import datetime
|
||||||
|
import socket
|
||||||
|
import six
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'format_pymux_string',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def format_pymux_string(pymux, string, window=None, pane=None):
|
||||||
|
"""
|
||||||
|
Apply pymux sting formatting. (Similar to tmux.)
|
||||||
|
E.g. #P is replaced by the index of the active pane.
|
||||||
|
|
||||||
|
We try to stay compatible with tmux, if possible.
|
||||||
|
One thing that we won't support (for now) is colors, because our styling
|
||||||
|
works different. (With a Style class.) On the other hand, in the future, we
|
||||||
|
could allow things like `#[token=Token.Title.PID]`. This gives a clean
|
||||||
|
separation of semantics and colors, making it easy to write different color
|
||||||
|
schemes.
|
||||||
|
"""
|
||||||
|
arrangement = pymux.arrangement
|
||||||
|
|
||||||
|
if window is None:
|
||||||
|
window = arrangement.get_active_window()
|
||||||
|
|
||||||
|
if pane is None:
|
||||||
|
pane = window.active_pane
|
||||||
|
|
||||||
|
def id_of_pane():
|
||||||
|
return '%s' % (pane.pane_id, )
|
||||||
|
|
||||||
|
def index_of_pane():
|
||||||
|
try:
|
||||||
|
return '%s' % (window.get_pane_index(pane), )
|
||||||
|
except ValueError:
|
||||||
|
return '/'
|
||||||
|
|
||||||
|
def index_of_window():
|
||||||
|
return '%s' % (window.index, )
|
||||||
|
|
||||||
|
def name_of_window():
|
||||||
|
return window.name or '(noname)'
|
||||||
|
|
||||||
|
def window_flags():
|
||||||
|
z = 'Z' if window.zoom else ''
|
||||||
|
|
||||||
|
if window == arrangement.get_active_window():
|
||||||
|
return '*' + z
|
||||||
|
elif window == arrangement.get_previous_active_window():
|
||||||
|
return '-' + z
|
||||||
|
else:
|
||||||
|
return z + ' '
|
||||||
|
|
||||||
|
def name_of_session():
|
||||||
|
return pymux.session_name
|
||||||
|
|
||||||
|
def title_of_pane():
|
||||||
|
return pane.process.screen.title
|
||||||
|
|
||||||
|
def hostname():
|
||||||
|
return socket.gethostname()
|
||||||
|
|
||||||
|
def literal():
|
||||||
|
return '#'
|
||||||
|
|
||||||
|
format_table = {
|
||||||
|
'#D': id_of_pane,
|
||||||
|
'#F': window_flags,
|
||||||
|
'#I': index_of_window,
|
||||||
|
'#P': index_of_pane,
|
||||||
|
'#S': name_of_session,
|
||||||
|
'#T': title_of_pane,
|
||||||
|
'#W': name_of_window,
|
||||||
|
'#h': hostname,
|
||||||
|
'##': literal,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Date/time formatting.
|
||||||
|
if '%' in string:
|
||||||
|
try:
|
||||||
|
if six.PY2:
|
||||||
|
string = datetime.datetime.now().strftime(
|
||||||
|
string.encode('utf-8')).decode('utf-8')
|
||||||
|
else:
|
||||||
|
string = datetime.datetime.now().strftime(string)
|
||||||
|
except ValueError: # strftime format ends with raw %
|
||||||
|
string = '<ValueError>'
|
||||||
|
|
||||||
|
# Apply '#' formatting.
|
||||||
|
for symbol, f in format_table.items():
|
||||||
|
if symbol in string:
|
||||||
|
string = string.replace(symbol, f())
|
||||||
|
|
||||||
|
return string
|
260
pymux/key_bindings.py
Normal file
260
pymux/key_bindings.py
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
"""
|
||||||
|
Key bindings.
|
||||||
|
"""
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from prompt_toolkit.filters import has_focus, Condition, has_selection
|
||||||
|
from prompt_toolkit.keys import Keys
|
||||||
|
from prompt_toolkit.selection import SelectionType
|
||||||
|
from prompt_toolkit.key_binding import KeyBindings, merge_key_bindings
|
||||||
|
|
||||||
|
from .enums import COMMAND, PROMPT
|
||||||
|
from .filters import WaitsForConfirmation, HasPrefix, InScrollBufferNotSearching
|
||||||
|
from .key_mappings import pymux_key_to_prompt_toolkit_key_sequence
|
||||||
|
from .commands.commands import call_command_handler
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'PymuxKeyBindings',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PymuxKeyBindings(object):
|
||||||
|
"""
|
||||||
|
Pymux key binding manager.
|
||||||
|
"""
|
||||||
|
def __init__(self, pymux):
|
||||||
|
self.pymux = pymux
|
||||||
|
|
||||||
|
def get_search_state():
|
||||||
|
" Return the currently active SearchState. (The one for the focused pane.) "
|
||||||
|
return pymux.arrangement.get_active_pane().search_state
|
||||||
|
|
||||||
|
self.custom_key_bindings = KeyBindings()
|
||||||
|
|
||||||
|
self.key_bindings = merge_key_bindings([
|
||||||
|
self._load_builtins(),
|
||||||
|
self.custom_key_bindings,
|
||||||
|
])
|
||||||
|
|
||||||
|
self._prefix = ('c-b', )
|
||||||
|
self._prefix_binding = None
|
||||||
|
|
||||||
|
# Load initial bindings.
|
||||||
|
self._load_prefix_binding()
|
||||||
|
|
||||||
|
# Custom user configured key bindings.
|
||||||
|
# { (needs_prefix, key) -> (command, handler) }
|
||||||
|
self.custom_bindings = {}
|
||||||
|
|
||||||
|
def _load_prefix_binding(self):
|
||||||
|
"""
|
||||||
|
Load the prefix key binding.
|
||||||
|
"""
|
||||||
|
pymux = self.pymux
|
||||||
|
|
||||||
|
# Remove previous binding.
|
||||||
|
if self._prefix_binding:
|
||||||
|
self.custom_key_bindings.remove_binding(self._prefix_binding)
|
||||||
|
|
||||||
|
# Create new Python binding.
|
||||||
|
@self.custom_key_bindings.add(*self._prefix, filter=
|
||||||
|
~(HasPrefix(pymux) | has_focus(COMMAND) | has_focus(PROMPT) |
|
||||||
|
WaitsForConfirmation(pymux)))
|
||||||
|
def enter_prefix_handler(event):
|
||||||
|
" Enter prefix mode. "
|
||||||
|
pymux.get_client_state().has_prefix = True
|
||||||
|
|
||||||
|
self._prefix_binding = enter_prefix_handler
|
||||||
|
|
||||||
|
@property
|
||||||
|
def prefix(self):
|
||||||
|
" Get the prefix key. "
|
||||||
|
return self._prefix
|
||||||
|
|
||||||
|
@prefix.setter
|
||||||
|
def prefix(self, keys):
|
||||||
|
"""
|
||||||
|
Set a new prefix key.
|
||||||
|
"""
|
||||||
|
assert isinstance(keys, tuple)
|
||||||
|
|
||||||
|
self._prefix = keys
|
||||||
|
self._load_prefix_binding()
|
||||||
|
|
||||||
|
def _load_builtins(self):
|
||||||
|
"""
|
||||||
|
Fill the Registry with the hard coded key bindings.
|
||||||
|
"""
|
||||||
|
pymux = self.pymux
|
||||||
|
kb = KeyBindings()
|
||||||
|
|
||||||
|
# Create filters.
|
||||||
|
has_prefix = HasPrefix(pymux)
|
||||||
|
waits_for_confirmation = WaitsForConfirmation(pymux)
|
||||||
|
prompt_or_command_focus = has_focus(COMMAND) | has_focus(PROMPT)
|
||||||
|
display_pane_numbers = Condition(lambda: pymux.display_pane_numbers)
|
||||||
|
in_scroll_buffer_not_searching = InScrollBufferNotSearching(pymux)
|
||||||
|
|
||||||
|
@kb.add(Keys.Any, filter=has_prefix)
|
||||||
|
def _(event):
|
||||||
|
" Ignore unknown Ctrl-B prefixed key sequences. "
|
||||||
|
pymux.get_client_state().has_prefix = False
|
||||||
|
|
||||||
|
@kb.add('c-c', filter=prompt_or_command_focus & ~has_prefix)
|
||||||
|
@kb.add('c-g', filter=prompt_or_command_focus & ~has_prefix)
|
||||||
|
# @kb.add('backspace', filter=has_focus(COMMAND) & ~has_prefix &
|
||||||
|
# Condition(lambda: cli.buffers[COMMAND].text == ''))
|
||||||
|
def _(event):
|
||||||
|
" Leave command mode. "
|
||||||
|
pymux.leave_command_mode(append_to_history=False)
|
||||||
|
|
||||||
|
@kb.add('y', filter=waits_for_confirmation)
|
||||||
|
@kb.add('Y', filter=waits_for_confirmation)
|
||||||
|
def _(event):
|
||||||
|
"""
|
||||||
|
Confirm command.
|
||||||
|
"""
|
||||||
|
client_state = pymux.get_client_state()
|
||||||
|
|
||||||
|
command = client_state.confirm_command
|
||||||
|
client_state.confirm_command = None
|
||||||
|
client_state.confirm_text = None
|
||||||
|
|
||||||
|
pymux.handle_command(command)
|
||||||
|
|
||||||
|
@kb.add('n', filter=waits_for_confirmation)
|
||||||
|
@kb.add('N', filter=waits_for_confirmation)
|
||||||
|
@kb.add('c-c' , filter=waits_for_confirmation)
|
||||||
|
def _(event):
|
||||||
|
"""
|
||||||
|
Cancel command.
|
||||||
|
"""
|
||||||
|
client_state = pymux.get_client_state()
|
||||||
|
client_state.confirm_command = None
|
||||||
|
client_state.confirm_text = None
|
||||||
|
|
||||||
|
@kb.add('c-c', filter=in_scroll_buffer_not_searching)
|
||||||
|
@kb.add('enter', filter=in_scroll_buffer_not_searching)
|
||||||
|
@kb.add('q', filter=in_scroll_buffer_not_searching)
|
||||||
|
def _(event):
|
||||||
|
" Exit scroll buffer. "
|
||||||
|
pane = pymux.arrangement.get_active_pane()
|
||||||
|
pane.exit_scroll_buffer()
|
||||||
|
|
||||||
|
@kb.add(' ', filter=in_scroll_buffer_not_searching)
|
||||||
|
def _(event):
|
||||||
|
" Enter selection mode when pressing space in copy mode. "
|
||||||
|
event.current_buffer.start_selection(selection_type=SelectionType.CHARACTERS)
|
||||||
|
|
||||||
|
@kb.add('enter', filter=in_scroll_buffer_not_searching & has_selection)
|
||||||
|
def _(event):
|
||||||
|
" Copy selection when pressing Enter. "
|
||||||
|
clipboard_data = event.current_buffer.copy_selection()
|
||||||
|
event.app.clipboard.set_data(clipboard_data)
|
||||||
|
|
||||||
|
@kb.add('v', filter=in_scroll_buffer_not_searching & has_selection)
|
||||||
|
def _(event):
|
||||||
|
" Toggle between selection types. "
|
||||||
|
types = [SelectionType.LINES, SelectionType.BLOCK, SelectionType.CHARACTERS]
|
||||||
|
selection_state = event.current_buffer.selection_state
|
||||||
|
|
||||||
|
try:
|
||||||
|
index = types.index(selection_state.type)
|
||||||
|
except ValueError: # Not in list.
|
||||||
|
index = 0
|
||||||
|
|
||||||
|
selection_state.type = types[(index + 1) % len(types)]
|
||||||
|
|
||||||
|
@Condition
|
||||||
|
def popup_displayed():
|
||||||
|
return self.pymux.get_client_state().display_popup
|
||||||
|
|
||||||
|
@kb.add('q', filter=popup_displayed, eager=True)
|
||||||
|
def _(event):
|
||||||
|
" Quit pop-up dialog. "
|
||||||
|
self.pymux.get_client_state().display_popup = False
|
||||||
|
|
||||||
|
@kb.add(Keys.Any, eager=True, filter=display_pane_numbers)
|
||||||
|
def _(event):
|
||||||
|
" When the pane numbers are shown. Any key press should hide them. "
|
||||||
|
pymux.display_pane_numbers = False
|
||||||
|
|
||||||
|
@Condition
|
||||||
|
def clock_displayed():
|
||||||
|
" "
|
||||||
|
pane = pymux.arrangement.get_active_pane()
|
||||||
|
return pane.clock_mode
|
||||||
|
|
||||||
|
@kb.add(Keys.Any, eager=True, filter=clock_displayed)
|
||||||
|
def _(event):
|
||||||
|
" When the clock is displayed. Any key press should hide it. "
|
||||||
|
pane = pymux.arrangement.get_active_pane()
|
||||||
|
pane.clock_mode = False
|
||||||
|
|
||||||
|
return kb
|
||||||
|
|
||||||
|
def add_custom_binding(self, key_name, command, arguments, needs_prefix=False):
|
||||||
|
"""
|
||||||
|
Add custom binding (for the "bind-key" command.)
|
||||||
|
Raises ValueError if the give `key_name` is an invalid name.
|
||||||
|
|
||||||
|
:param key_name: Pymux key name, for instance "C-a" or "M-x".
|
||||||
|
"""
|
||||||
|
assert isinstance(key_name, six.text_type)
|
||||||
|
assert isinstance(command, six.text_type)
|
||||||
|
assert isinstance(arguments, list)
|
||||||
|
|
||||||
|
# Unbind previous key.
|
||||||
|
self.remove_custom_binding(key_name, needs_prefix=needs_prefix)
|
||||||
|
|
||||||
|
# Translate the pymux key name into a prompt_toolkit key sequence.
|
||||||
|
# (Can raise ValueError.)
|
||||||
|
keys_sequence = pymux_key_to_prompt_toolkit_key_sequence(key_name)
|
||||||
|
|
||||||
|
# Create handler and add to Registry.
|
||||||
|
if needs_prefix:
|
||||||
|
filter = HasPrefix(self.pymux)
|
||||||
|
else:
|
||||||
|
filter = ~HasPrefix(self.pymux)
|
||||||
|
|
||||||
|
filter = filter & ~(WaitsForConfirmation(self.pymux) |
|
||||||
|
has_focus(COMMAND) | has_focus(PROMPT))
|
||||||
|
|
||||||
|
def key_handler(event):
|
||||||
|
" The actual key handler. "
|
||||||
|
call_command_handler(command, self.pymux, arguments)
|
||||||
|
self.pymux.get_client_state().has_prefix = False
|
||||||
|
|
||||||
|
self.custom_key_bindings.add(*keys_sequence, filter=filter)(key_handler)
|
||||||
|
|
||||||
|
# Store key in `custom_bindings` in order to be able to call
|
||||||
|
# "unbind-key" later on.
|
||||||
|
k = (needs_prefix, key_name)
|
||||||
|
self.custom_bindings[k] = CustomBinding(key_handler, command, arguments)
|
||||||
|
|
||||||
|
def remove_custom_binding(self, key_name, needs_prefix=False):
|
||||||
|
"""
|
||||||
|
Remove custom key binding for a key.
|
||||||
|
|
||||||
|
:param key_name: Pymux key name, for instance "C-A".
|
||||||
|
"""
|
||||||
|
k = (needs_prefix, key_name)
|
||||||
|
|
||||||
|
if k in self.custom_bindings:
|
||||||
|
self.custom_key_bindings.remove(self.custom_bindings[k].handler)
|
||||||
|
del self.custom_bindings[k]
|
||||||
|
|
||||||
|
|
||||||
|
class CustomBinding(object):
|
||||||
|
"""
|
||||||
|
Record for storing a single custom key binding.
|
||||||
|
"""
|
||||||
|
def __init__(self, handler, command, arguments):
|
||||||
|
assert callable(handler)
|
||||||
|
assert isinstance(command, six.text_type)
|
||||||
|
assert isinstance(arguments, list)
|
||||||
|
|
||||||
|
self.handler = handler
|
||||||
|
self.command = command
|
||||||
|
self.arguments = arguments
|
231
pymux/key_mappings.py
Normal file
231
pymux/key_mappings.py
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
"""
|
||||||
|
Mapping between vt100 key sequences, the prompt_toolkit key constants and the
|
||||||
|
Pymux namings. (Those namings are kept compatible with tmux.)
|
||||||
|
"""
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from prompt_toolkit.keys import Keys
|
||||||
|
from prompt_toolkit.input.vt100_parser import ANSI_SEQUENCES
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'pymux_key_to_prompt_toolkit_key_sequence',
|
||||||
|
'prompt_toolkit_key_to_vt100_key',
|
||||||
|
'PYMUX_TO_PROMPT_TOOLKIT_KEYS',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def pymux_key_to_prompt_toolkit_key_sequence(key):
|
||||||
|
"""
|
||||||
|
Turn a pymux description of a key. E.g. "C-a" or "M-x" into a
|
||||||
|
prompt-toolkit key sequence.
|
||||||
|
|
||||||
|
Raises `ValueError` if the key is not known.
|
||||||
|
"""
|
||||||
|
# Make the c- and m- prefixes case insensitive.
|
||||||
|
if key.lower().startswith('m-c-'):
|
||||||
|
key = 'M-C-' + key[4:]
|
||||||
|
elif key.lower().startswith('c-'):
|
||||||
|
key = 'C-' + key[2:]
|
||||||
|
elif key.lower().startswith('m-'):
|
||||||
|
key = 'M-' + key[2:]
|
||||||
|
|
||||||
|
# Lookup key.
|
||||||
|
try:
|
||||||
|
return PYMUX_TO_PROMPT_TOOLKIT_KEYS[key]
|
||||||
|
except KeyError:
|
||||||
|
if len(key) == 1:
|
||||||
|
return (key, )
|
||||||
|
else:
|
||||||
|
raise ValueError('Unknown key: %r' % (key, ))
|
||||||
|
|
||||||
|
|
||||||
|
# Create a mapping from prompt_toolkit keys to their ANSI sequences.
|
||||||
|
# TODO: This is not completely correct yet. It doesn't take
|
||||||
|
# cursor/application mode into account. Create new tables for this.
|
||||||
|
_PROMPT_TOOLKIT_KEY_TO_VT100 = dict(
|
||||||
|
(key, vt100_data) for vt100_data, key in ANSI_SEQUENCES.items())
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_toolkit_key_to_vt100_key(key, application_mode=False):
|
||||||
|
"""
|
||||||
|
Turn a prompt toolkit key. (E.g Keys.ControlB) into a Vt100 key sequence.
|
||||||
|
(E.g. \x1b[A.)
|
||||||
|
"""
|
||||||
|
application_mode_keys = {
|
||||||
|
Keys.Up: '\x1bOA',
|
||||||
|
Keys.Left: '\x1bOD',
|
||||||
|
Keys.Right: '\x1bOC',
|
||||||
|
Keys.Down: '\x1bOB',
|
||||||
|
}
|
||||||
|
|
||||||
|
if key == Keys.ControlJ:
|
||||||
|
# Required for redis-cli. This can be removed when prompt_toolkit stops
|
||||||
|
# replacing \r by \n.
|
||||||
|
return '\r'
|
||||||
|
|
||||||
|
if key == '\n':
|
||||||
|
return '\r'
|
||||||
|
|
||||||
|
elif application_mode and key in application_mode_keys:
|
||||||
|
return application_mode_keys.get(key)
|
||||||
|
else:
|
||||||
|
return _PROMPT_TOOLKIT_KEY_TO_VT100.get(key, key)
|
||||||
|
|
||||||
|
|
||||||
|
PYMUX_TO_PROMPT_TOOLKIT_KEYS = {
|
||||||
|
'Space': (' '),
|
||||||
|
|
||||||
|
'C-a': (Keys.ControlA, ),
|
||||||
|
'C-b': (Keys.ControlB, ),
|
||||||
|
'C-c': (Keys.ControlC, ),
|
||||||
|
'C-d': (Keys.ControlD, ),
|
||||||
|
'C-e': (Keys.ControlE, ),
|
||||||
|
'C-f': (Keys.ControlF, ),
|
||||||
|
'C-g': (Keys.ControlG, ),
|
||||||
|
'C-h': (Keys.ControlH, ),
|
||||||
|
'C-i': (Keys.ControlI, ),
|
||||||
|
'C-j': (Keys.ControlJ, ),
|
||||||
|
'C-k': (Keys.ControlK, ),
|
||||||
|
'C-l': (Keys.ControlL, ),
|
||||||
|
'C-m': (Keys.ControlM, ),
|
||||||
|
'C-n': (Keys.ControlN, ),
|
||||||
|
'C-o': (Keys.ControlO, ),
|
||||||
|
'C-p': (Keys.ControlP, ),
|
||||||
|
'C-q': (Keys.ControlQ, ),
|
||||||
|
'C-r': (Keys.ControlR, ),
|
||||||
|
'C-s': (Keys.ControlS, ),
|
||||||
|
'C-t': (Keys.ControlT, ),
|
||||||
|
'C-u': (Keys.ControlU, ),
|
||||||
|
'C-v': (Keys.ControlV, ),
|
||||||
|
'C-w': (Keys.ControlW, ),
|
||||||
|
'C-x': (Keys.ControlX, ),
|
||||||
|
'C-y': (Keys.ControlY, ),
|
||||||
|
'C-z': (Keys.ControlZ, ),
|
||||||
|
|
||||||
|
'C-Left': (Keys.ControlLeft, ),
|
||||||
|
'C-Right': (Keys.ControlRight, ),
|
||||||
|
'C-Up': (Keys.ControlUp, ),
|
||||||
|
'C-Down': (Keys.ControlDown, ),
|
||||||
|
'C-\\': (Keys.ControlBackslash, ),
|
||||||
|
|
||||||
|
'S-Left': (Keys.ShiftLeft, ),
|
||||||
|
'S-Right': (Keys.ShiftRight, ),
|
||||||
|
'S-Up': (Keys.ShiftUp, ),
|
||||||
|
'S-Down': (Keys.ShiftDown, ),
|
||||||
|
|
||||||
|
'M-C-a': (Keys.Escape, Keys.ControlA, ),
|
||||||
|
'M-C-b': (Keys.Escape, Keys.ControlB, ),
|
||||||
|
'M-C-c': (Keys.Escape, Keys.ControlC, ),
|
||||||
|
'M-C-d': (Keys.Escape, Keys.ControlD, ),
|
||||||
|
'M-C-e': (Keys.Escape, Keys.ControlE, ),
|
||||||
|
'M-C-f': (Keys.Escape, Keys.ControlF, ),
|
||||||
|
'M-C-g': (Keys.Escape, Keys.ControlG, ),
|
||||||
|
'M-C-h': (Keys.Escape, Keys.ControlH, ),
|
||||||
|
'M-C-i': (Keys.Escape, Keys.ControlI, ),
|
||||||
|
'M-C-j': (Keys.Escape, Keys.ControlJ, ),
|
||||||
|
'M-C-k': (Keys.Escape, Keys.ControlK, ),
|
||||||
|
'M-C-l': (Keys.Escape, Keys.ControlL, ),
|
||||||
|
'M-C-m': (Keys.Escape, Keys.ControlM, ),
|
||||||
|
'M-C-n': (Keys.Escape, Keys.ControlN, ),
|
||||||
|
'M-C-o': (Keys.Escape, Keys.ControlO, ),
|
||||||
|
'M-C-p': (Keys.Escape, Keys.ControlP, ),
|
||||||
|
'M-C-q': (Keys.Escape, Keys.ControlQ, ),
|
||||||
|
'M-C-r': (Keys.Escape, Keys.ControlR, ),
|
||||||
|
'M-C-s': (Keys.Escape, Keys.ControlS, ),
|
||||||
|
'M-C-t': (Keys.Escape, Keys.ControlT, ),
|
||||||
|
'M-C-u': (Keys.Escape, Keys.ControlU, ),
|
||||||
|
'M-C-v': (Keys.Escape, Keys.ControlV, ),
|
||||||
|
'M-C-w': (Keys.Escape, Keys.ControlW, ),
|
||||||
|
'M-C-x': (Keys.Escape, Keys.ControlX, ),
|
||||||
|
'M-C-y': (Keys.Escape, Keys.ControlY, ),
|
||||||
|
'M-C-z': (Keys.Escape, Keys.ControlZ, ),
|
||||||
|
|
||||||
|
'M-C-Left': (Keys.Escape, Keys.ControlLeft, ),
|
||||||
|
'M-C-Right': (Keys.Escape, Keys.ControlRight, ),
|
||||||
|
'M-C-Up': (Keys.Escape, Keys.ControlUp, ),
|
||||||
|
'M-C-Down': (Keys.Escape, Keys.ControlDown, ),
|
||||||
|
'M-C-\\': (Keys.Escape, Keys.ControlBackslash, ),
|
||||||
|
|
||||||
|
'M-a': (Keys.Escape, 'a'),
|
||||||
|
'M-b': (Keys.Escape, 'b'),
|
||||||
|
'M-c': (Keys.Escape, 'c'),
|
||||||
|
'M-d': (Keys.Escape, 'd'),
|
||||||
|
'M-e': (Keys.Escape, 'e'),
|
||||||
|
'M-f': (Keys.Escape, 'f'),
|
||||||
|
'M-g': (Keys.Escape, 'g'),
|
||||||
|
'M-h': (Keys.Escape, 'h'),
|
||||||
|
'M-i': (Keys.Escape, 'i'),
|
||||||
|
'M-j': (Keys.Escape, 'j'),
|
||||||
|
'M-k': (Keys.Escape, 'k'),
|
||||||
|
'M-l': (Keys.Escape, 'l'),
|
||||||
|
'M-m': (Keys.Escape, 'm'),
|
||||||
|
'M-n': (Keys.Escape, 'n'),
|
||||||
|
'M-o': (Keys.Escape, 'o'),
|
||||||
|
'M-p': (Keys.Escape, 'p'),
|
||||||
|
'M-q': (Keys.Escape, 'q'),
|
||||||
|
'M-r': (Keys.Escape, 'r'),
|
||||||
|
'M-s': (Keys.Escape, 's'),
|
||||||
|
'M-t': (Keys.Escape, 't'),
|
||||||
|
'M-u': (Keys.Escape, 'u'),
|
||||||
|
'M-v': (Keys.Escape, 'v'),
|
||||||
|
'M-w': (Keys.Escape, 'w'),
|
||||||
|
'M-x': (Keys.Escape, 'x'),
|
||||||
|
'M-y': (Keys.Escape, 'y'),
|
||||||
|
'M-z': (Keys.Escape, 'z'),
|
||||||
|
|
||||||
|
'M-0': (Keys.Escape, '0'),
|
||||||
|
'M-1': (Keys.Escape, '1'),
|
||||||
|
'M-2': (Keys.Escape, '2'),
|
||||||
|
'M-3': (Keys.Escape, '3'),
|
||||||
|
'M-4': (Keys.Escape, '4'),
|
||||||
|
'M-5': (Keys.Escape, '5'),
|
||||||
|
'M-6': (Keys.Escape, '6'),
|
||||||
|
'M-7': (Keys.Escape, '7'),
|
||||||
|
'M-8': (Keys.Escape, '8'),
|
||||||
|
'M-9': (Keys.Escape, '9'),
|
||||||
|
|
||||||
|
'M-Up': (Keys.Escape, Keys.Up),
|
||||||
|
'M-Down': (Keys.Escape, Keys.Down, ),
|
||||||
|
'M-Left': (Keys.Escape, Keys.Left, ),
|
||||||
|
'M-Right': (Keys.Escape, Keys.Right, ),
|
||||||
|
'Left': (Keys.Left, ),
|
||||||
|
'Right': (Keys.Right, ),
|
||||||
|
'Up': (Keys.Up, ),
|
||||||
|
'Down': (Keys.Down, ),
|
||||||
|
'BSpace': (Keys.Backspace, ),
|
||||||
|
'BTab': (Keys.BackTab, ),
|
||||||
|
'DC': (Keys.Delete, ),
|
||||||
|
'IC': (Keys.Insert, ),
|
||||||
|
'End': (Keys.End, ),
|
||||||
|
'Enter': (Keys.ControlJ, ),
|
||||||
|
'Home': (Keys.Home, ),
|
||||||
|
'Escape': (Keys.Escape, ),
|
||||||
|
'Tab': (Keys.Tab, ),
|
||||||
|
|
||||||
|
'F1': (Keys.F1, ),
|
||||||
|
'F2': (Keys.F2, ),
|
||||||
|
'F3': (Keys.F3, ),
|
||||||
|
'F4': (Keys.F4, ),
|
||||||
|
'F5': (Keys.F5, ),
|
||||||
|
'F6': (Keys.F6, ),
|
||||||
|
'F7': (Keys.F7, ),
|
||||||
|
'F8': (Keys.F8, ),
|
||||||
|
'F9': (Keys.F9, ),
|
||||||
|
'F10': (Keys.F10, ),
|
||||||
|
'F11': (Keys.F11, ),
|
||||||
|
'F12': (Keys.F12, ),
|
||||||
|
'F13': (Keys.F13, ),
|
||||||
|
'F14': (Keys.F14, ),
|
||||||
|
'F15': (Keys.F15, ),
|
||||||
|
'F16': (Keys.F16, ),
|
||||||
|
'F17': (Keys.F17, ),
|
||||||
|
'F18': (Keys.F18, ),
|
||||||
|
'F19': (Keys.F19, ),
|
||||||
|
'F20': (Keys.F20, ),
|
||||||
|
|
||||||
|
'NPage': (Keys.PageDown, ),
|
||||||
|
'PageDown': (Keys.PageDown, ),
|
||||||
|
'PgDn': (Keys.PageDown, ),
|
||||||
|
'PPage': (Keys.PageUp, ),
|
||||||
|
'PageUp': (Keys.PageUp, ),
|
||||||
|
'PgUp': (Keys.PageUp, ),
|
||||||
|
}
|
981
pymux/layout.py
Normal file
981
pymux/layout.py
Normal file
@ -0,0 +1,981 @@
|
|||||||
|
# encoding: utf-8
|
||||||
|
"""
|
||||||
|
The layout engine. This builds the prompt_toolkit layout.
|
||||||
|
"""
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from prompt_toolkit.application.current import get_app
|
||||||
|
from prompt_toolkit.filters import Condition, has_focus
|
||||||
|
from prompt_toolkit.formatted_text import FormattedText, HTML
|
||||||
|
from prompt_toolkit.layout.containers import VSplit, HSplit, Window, FloatContainer, Float, ConditionalContainer, Container, WindowAlign, to_container
|
||||||
|
from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl
|
||||||
|
from prompt_toolkit.layout.dimension import Dimension
|
||||||
|
from prompt_toolkit.layout.dimension import Dimension as D
|
||||||
|
from prompt_toolkit.layout.dimension import to_dimension, is_dimension
|
||||||
|
from prompt_toolkit.layout.menus import CompletionsMenu
|
||||||
|
from prompt_toolkit.layout.processors import BeforeInput, ShowArg, AppendAutoSuggestion, Processor, Transformation, HighlightSelectionProcessor
|
||||||
|
from prompt_toolkit.layout.screen import Char
|
||||||
|
from prompt_toolkit.mouse_events import MouseEventType
|
||||||
|
from prompt_toolkit.widgets import FormattedTextToolbar, TextArea, Dialog, SearchToolbar
|
||||||
|
|
||||||
|
from six.moves import range
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
import pymux.arrangement as arrangement
|
||||||
|
import datetime
|
||||||
|
import weakref
|
||||||
|
import six
|
||||||
|
|
||||||
|
from .filters import WaitsForConfirmation
|
||||||
|
from .format import format_pymux_string
|
||||||
|
from .log import logger
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'LayoutManager',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Justify:
|
||||||
|
" Justify enum for the status bar. "
|
||||||
|
LEFT = 'left'
|
||||||
|
CENTER = 'center'
|
||||||
|
RIGHT = 'right'
|
||||||
|
|
||||||
|
_ALL = [LEFT, CENTER, RIGHT]
|
||||||
|
|
||||||
|
|
||||||
|
class Z_INDEX:
|
||||||
|
HIGHLIGHTED_BORDER = 2
|
||||||
|
STATUS_BAR = 5
|
||||||
|
COMMAND_LINE = 6
|
||||||
|
MESSAGE_TOOLBAR = 7
|
||||||
|
WINDOW_TITLE_BAR = 8
|
||||||
|
POPUP = 9
|
||||||
|
######################################################################################################## DECODED
|
||||||
|
DECODED_TOOLBAR = 10
|
||||||
|
######################################################################################################## DECODED
|
||||||
|
|
||||||
|
class Background(Container):
|
||||||
|
"""
|
||||||
|
Generate the background of dots, which becomes visible when several clients
|
||||||
|
are attached and not all of them have the same size.
|
||||||
|
|
||||||
|
(This is implemented as a Container, rather than a UIControl wrapped in a
|
||||||
|
Window, because it can be done very effecient this way.)
|
||||||
|
"""
|
||||||
|
def reset(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def preferred_width(self, max_available_width):
|
||||||
|
return D()
|
||||||
|
|
||||||
|
def preferred_height(self, width, max_available_height):
|
||||||
|
return D()
|
||||||
|
|
||||||
|
def write_to_screen(self, screen, mouse_handlers, write_position,
|
||||||
|
parent_style, erase_bg, z_index):
|
||||||
|
" Fill the whole area of write_position with dots. "
|
||||||
|
default_char = Char(' ', 'class:background')
|
||||||
|
dot = Char('.', 'class:background')
|
||||||
|
|
||||||
|
ypos = write_position.ypos
|
||||||
|
xpos = write_position.xpos
|
||||||
|
|
||||||
|
for y in range(ypos, ypos + write_position.height):
|
||||||
|
row = screen.data_buffer[y]
|
||||||
|
|
||||||
|
for x in range(xpos, xpos + write_position.width):
|
||||||
|
row[x] = dot if (x + y) % 3 == 0 else default_char
|
||||||
|
|
||||||
|
def get_children(self):
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
# Numbers for the clock and pane numbering.
|
||||||
|
_numbers = list(zip(*[ # (Transpose x/y.)
|
||||||
|
['#####', ' #', '#####', '#####', '# #', '#####', '#####', '#####', '#####', '#####'],
|
||||||
|
['# #', ' #', ' #', ' #', '# #', '# ', '# ', ' #', '# #', '# #'],
|
||||||
|
['# #', ' #', '#####', '#####', '#####', '#####', '#####', ' #', '#####', '#####'],
|
||||||
|
['# #', ' #', '# ', ' #', ' #', ' #', '# #', ' #', '# #', ' #'],
|
||||||
|
['#####', ' #', '#####', '#####', ' #', '#####', '#####', ' #', '#####', '#####'],
|
||||||
|
]))
|
||||||
|
|
||||||
|
|
||||||
|
def _draw_number(screen, x_offset, y_offset, number, style='class:clock',
|
||||||
|
transparent=False):
|
||||||
|
" Write number at position. "
|
||||||
|
fg = Char(' ', 'class:clock')
|
||||||
|
bg = Char(' ', '')
|
||||||
|
|
||||||
|
for y, row in enumerate(_numbers[number]):
|
||||||
|
screen_row = screen.data_buffer[y + y_offset]
|
||||||
|
for x, n in enumerate(row):
|
||||||
|
if n == '#':
|
||||||
|
screen_row[x + x_offset] = fg
|
||||||
|
elif not transparent:
|
||||||
|
screen_row[x + x_offset] = bg
|
||||||
|
|
||||||
|
|
||||||
|
class BigClock(Container):
|
||||||
|
"""
|
||||||
|
Display a big clock.
|
||||||
|
"""
|
||||||
|
WIDTH = 28
|
||||||
|
HEIGHT = 5
|
||||||
|
|
||||||
|
def __init__(self, on_click):
|
||||||
|
assert callable(on_click)
|
||||||
|
self.on_click = on_click
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def write_to_screen(self, screen, mouse_handlers, write_position,
|
||||||
|
parent_style, erase_bg, z_index):
|
||||||
|
xpos = write_position.xpos
|
||||||
|
ypos = write_position.ypos
|
||||||
|
|
||||||
|
# Erase background.
|
||||||
|
bg = Char(' ', '')
|
||||||
|
|
||||||
|
def draw_func():
|
||||||
|
for y in range(ypos, self.HEIGHT + ypos):
|
||||||
|
row = screen.data_buffer[y]
|
||||||
|
for x in range(xpos, xpos + self.WIDTH):
|
||||||
|
row[x] = bg
|
||||||
|
|
||||||
|
# Display time.
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
_draw_number(screen, xpos + 0, ypos, now.hour // 10)
|
||||||
|
_draw_number(screen, xpos + 6, ypos, now.hour % 10)
|
||||||
|
_draw_number(screen, xpos + 16, ypos, now.minute // 10)
|
||||||
|
_draw_number(screen, xpos + 23, ypos, now.minute % 10)
|
||||||
|
|
||||||
|
# Add a colon
|
||||||
|
screen.data_buffer[ypos + 1][xpos + 13] = Char(' ', 'class:clock')
|
||||||
|
screen.data_buffer[ypos + 3][xpos + 13] = Char(' ', 'class:clock')
|
||||||
|
|
||||||
|
screen.width = self.WIDTH
|
||||||
|
screen.height = self.HEIGHT
|
||||||
|
|
||||||
|
mouse_handlers.set_mouse_handler_for_range(
|
||||||
|
x_min=xpos,
|
||||||
|
x_max=xpos + write_position.width,
|
||||||
|
y_min=ypos,
|
||||||
|
y_max=ypos + write_position.height,
|
||||||
|
handler=self._mouse_handler)
|
||||||
|
|
||||||
|
screen.draw_with_z_index(z_index=z_index, draw_func=draw_func)
|
||||||
|
|
||||||
|
def _mouse_handler(self, cli, mouse_event):
|
||||||
|
" Click callback. "
|
||||||
|
if mouse_event.event_type == MouseEventType.MOUSE_UP:
|
||||||
|
self.on_click(cli)
|
||||||
|
else:
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def preferred_width(self, max_available_width):
|
||||||
|
return D.exact(BigClock.WIDTH)
|
||||||
|
|
||||||
|
def preferred_height(self, width, max_available_height):
|
||||||
|
return D.exact(BigClock.HEIGHT)
|
||||||
|
|
||||||
|
def get_children(self):
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class PaneNumber(Container): # XXX: make FormattedTextControl
|
||||||
|
"""
|
||||||
|
Number of panes, to be drawn in the middle of the pane.
|
||||||
|
"""
|
||||||
|
WIDTH = 5
|
||||||
|
HEIGHT = 5
|
||||||
|
|
||||||
|
def __init__(self, pymux, arrangement_pane):
|
||||||
|
self.pymux = pymux
|
||||||
|
self.arrangement_pane = arrangement_pane
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _get_index(self):
|
||||||
|
window = self.pymux.arrangement.get_active_window()
|
||||||
|
try:
|
||||||
|
return window.get_pane_index(self.arrangement_pane)
|
||||||
|
except ValueError:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def preferred_width(self, max_available_width):
|
||||||
|
# Enough to display all the digits.
|
||||||
|
return Dimension.exact(6 * len('%s' % self._get_index()) - 1)
|
||||||
|
|
||||||
|
def preferred_height(self, width, max_available_height):
|
||||||
|
return Dimension.exact(self.HEIGHT)
|
||||||
|
|
||||||
|
def write_to_screen(self, screen, mouse_handlers, write_position,
|
||||||
|
parent_style, erase_bg, z_index):
|
||||||
|
style = 'class:panenumber'
|
||||||
|
|
||||||
|
def draw_func():
|
||||||
|
for i, d in enumerate('%s' % (self._get_index(),)):
|
||||||
|
_draw_number(screen, write_position.xpos + i * 6, write_position.ypos,
|
||||||
|
int(d), style=style, transparent=True)
|
||||||
|
|
||||||
|
screen.draw_with_z_index(z_index=z_index, draw_func=draw_func)
|
||||||
|
|
||||||
|
def get_children(self):
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class MessageToolbar(FormattedTextToolbar):
|
||||||
|
"""
|
||||||
|
Pop-up (at the bottom) for showing error/status messages.
|
||||||
|
"""
|
||||||
|
def __init__(self, client_state):
|
||||||
|
def get_message():
|
||||||
|
# If there is a message to be shown for this client, show that.
|
||||||
|
if client_state.message:
|
||||||
|
return client_state.message
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def get_tokens():
|
||||||
|
message = get_message()
|
||||||
|
if message:
|
||||||
|
return FormattedText([
|
||||||
|
('class:message', message),
|
||||||
|
('[SetCursorPosition]', ''),
|
||||||
|
('class:message', ' '),
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@Condition
|
||||||
|
def is_visible():
|
||||||
|
return bool(get_message())
|
||||||
|
|
||||||
|
super(MessageToolbar, self).__init__(get_tokens)
|
||||||
|
|
||||||
|
############################################################################################################ DECODED
|
||||||
|
|
||||||
|
class DecodedToolbar(FormattedTextToolbar):
|
||||||
|
"""
|
||||||
|
Pop-up (at the bottom) for showing decoded messages.
|
||||||
|
"""
|
||||||
|
def __init__(self, client_state):
|
||||||
|
def get_message():
|
||||||
|
# If there is a decoded to be shown for this client, show that.
|
||||||
|
if client_state.decoded:
|
||||||
|
return client_state.decoded
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def get_tokens():
|
||||||
|
decoded = get_message()
|
||||||
|
if decoded:
|
||||||
|
return FormattedText([
|
||||||
|
('class:decoded', decoded),
|
||||||
|
('[SetCursorPosition]', ''),
|
||||||
|
('class:decoded', ' '),
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@Condition
|
||||||
|
def is_visible():
|
||||||
|
return bool(get_message())
|
||||||
|
|
||||||
|
super(DecodedToolbar, self).__init__(get_tokens)
|
||||||
|
|
||||||
|
############################################################################################################ DECODED
|
||||||
|
|
||||||
|
class LayoutManager(object):
|
||||||
|
"""
|
||||||
|
The main layout class, that contains the whole Pymux layout.
|
||||||
|
"""
|
||||||
|
def __init__(self, pymux, client_state):
|
||||||
|
self.pymux = pymux
|
||||||
|
self.client_state = client_state
|
||||||
|
|
||||||
|
# Popup dialog for displaying keys, etc...
|
||||||
|
search_textarea = SearchToolbar()
|
||||||
|
self._popup_textarea = TextArea(scrollbar=True, read_only=True, search_field=search_textarea)
|
||||||
|
self.popup_dialog = Dialog(
|
||||||
|
title='Keys',
|
||||||
|
body=HSplit([
|
||||||
|
Window(FormattedTextControl(text=''), height=1), # 1 line margin.
|
||||||
|
self._popup_textarea,
|
||||||
|
search_textarea,
|
||||||
|
Window(
|
||||||
|
FormattedTextControl(
|
||||||
|
text=HTML('Press [<b>q</b>] to quit or [<b>/</b>] for searching.')),
|
||||||
|
align=WindowAlign.CENTER,
|
||||||
|
height=1)
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
self.layout = self._create_layout()
|
||||||
|
|
||||||
|
# Keep track of render information.
|
||||||
|
self.pane_write_positions = {}
|
||||||
|
|
||||||
|
def reset_write_positions(self):
|
||||||
|
"""
|
||||||
|
Clear write positions right before rendering. (They are populated
|
||||||
|
during rendering).
|
||||||
|
"""
|
||||||
|
self.pane_write_positions = {}
|
||||||
|
|
||||||
|
def display_popup(self, title, content):
|
||||||
|
"""
|
||||||
|
Display a pop-up dialog.
|
||||||
|
"""
|
||||||
|
assert isinstance(title, six.text_type)
|
||||||
|
assert isinstance(content, six.text_type)
|
||||||
|
|
||||||
|
self.popup_dialog.title = title
|
||||||
|
self._popup_textarea.text = content
|
||||||
|
self.client_state.display_popup = True
|
||||||
|
get_app().layout.focus(self._popup_textarea)
|
||||||
|
|
||||||
|
def _create_select_window_handler(self, window):
|
||||||
|
" Return a mouse handler that selects the given window when clicking. "
|
||||||
|
def handler(mouse_event):
|
||||||
|
if mouse_event.event_type == MouseEventType.MOUSE_DOWN:
|
||||||
|
self.pymux.arrangement.set_active_window(window)
|
||||||
|
self.pymux.invalidate()
|
||||||
|
else:
|
||||||
|
return NotImplemented # Event not handled here.
|
||||||
|
return handler
|
||||||
|
|
||||||
|
def _get_status_tokens(self):
|
||||||
|
" The tokens for the status bar. "
|
||||||
|
result = []
|
||||||
|
|
||||||
|
# Display panes.
|
||||||
|
for i, w in enumerate(self.pymux.arrangement.windows):
|
||||||
|
if i > 0:
|
||||||
|
result.append(('', ' '))
|
||||||
|
|
||||||
|
if w == self.pymux.arrangement.get_active_window():
|
||||||
|
style = 'class:window.current'
|
||||||
|
format_str = self.pymux.window_status_current_format
|
||||||
|
|
||||||
|
else:
|
||||||
|
style = 'class:window'
|
||||||
|
format_str = self.pymux.window_status_format
|
||||||
|
|
||||||
|
result.append((
|
||||||
|
style,
|
||||||
|
format_pymux_string(self.pymux, format_str, window=w),
|
||||||
|
self._create_select_window_handler(w)))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _get_status_left_tokens(self):
|
||||||
|
return format_pymux_string(self.pymux, self.pymux.status_left)
|
||||||
|
|
||||||
|
def _get_status_right_tokens(self):
|
||||||
|
return format_pymux_string(self.pymux, self.pymux.status_right)
|
||||||
|
|
||||||
|
def _get_align(self):
|
||||||
|
if self.pymux.status_justify == Justify.RIGHT:
|
||||||
|
return WindowAlign.RIGHT
|
||||||
|
elif self.pymux.status_justify == Justify.CENTER:
|
||||||
|
return WindowAlign.CENTER
|
||||||
|
else:
|
||||||
|
return WindowAlign.LEFT
|
||||||
|
|
||||||
|
def _before_prompt_command_tokens(self):
|
||||||
|
return [('class:commandline.prompt', '%s ' % (self.client_state.prompt_text, ))]
|
||||||
|
|
||||||
|
def _create_layout(self):
|
||||||
|
"""
|
||||||
|
Generate the main prompt_toolkit layout.
|
||||||
|
"""
|
||||||
|
waits_for_confirmation = WaitsForConfirmation(self.pymux)
|
||||||
|
|
||||||
|
return FloatContainer(
|
||||||
|
content=HSplit([
|
||||||
|
# The main window.
|
||||||
|
FloatContainer(
|
||||||
|
Background(),
|
||||||
|
floats=[
|
||||||
|
Float(width=lambda: self.pymux.get_window_size().columns,
|
||||||
|
height=lambda: self.pymux.get_window_size().rows,
|
||||||
|
content=DynamicBody(self.pymux))
|
||||||
|
]),
|
||||||
|
# Status bar.
|
||||||
|
ConditionalContainer(
|
||||||
|
content=VSplit([
|
||||||
|
# Left.
|
||||||
|
Window(
|
||||||
|
height=1,
|
||||||
|
width=(lambda: D(max=self.pymux.status_left_length)),
|
||||||
|
dont_extend_width=True,
|
||||||
|
content=FormattedTextControl(self._get_status_left_tokens)),
|
||||||
|
# List of windows in the middle.
|
||||||
|
Window(
|
||||||
|
height=1,
|
||||||
|
char=' ',
|
||||||
|
align=self._get_align,
|
||||||
|
content=FormattedTextControl(self._get_status_tokens)),
|
||||||
|
# Right.
|
||||||
|
Window(
|
||||||
|
height=1,
|
||||||
|
width=(lambda: D(max=self.pymux.status_right_length)),
|
||||||
|
dont_extend_width=True,
|
||||||
|
align=WindowAlign.RIGHT,
|
||||||
|
content=FormattedTextControl(self._get_status_right_tokens))
|
||||||
|
], z_index=Z_INDEX.STATUS_BAR, style='class:statusbar'),
|
||||||
|
filter=Condition(lambda: self.pymux.enable_status),
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
floats=[
|
||||||
|
############################################################################################ DECODED
|
||||||
|
Float(bottom=1, right=1, z_index=Z_INDEX.DECODED_TOOLBAR,
|
||||||
|
content=DecodedToolbar(self.client_state)),
|
||||||
|
############################################################################################ DECODED
|
||||||
|
Float(bottom=1, left=0, z_index=Z_INDEX.MESSAGE_TOOLBAR,
|
||||||
|
content=MessageToolbar(self.client_state)),
|
||||||
|
Float(left=0, right=0, bottom=0, content=HSplit([
|
||||||
|
# Wait for confirmation toolbar.
|
||||||
|
ConditionalContainer(
|
||||||
|
content=Window(
|
||||||
|
height=1,
|
||||||
|
content=ConfirmationToolbar(self.pymux, self.client_state),
|
||||||
|
z_index=Z_INDEX.COMMAND_LINE,
|
||||||
|
),
|
||||||
|
filter=waits_for_confirmation,
|
||||||
|
),
|
||||||
|
# ':' prompt toolbar.
|
||||||
|
ConditionalContainer(
|
||||||
|
content=Window(
|
||||||
|
height=D(min=1), # Can be more if the command is multiline.
|
||||||
|
style='class:commandline',
|
||||||
|
dont_extend_height=True,
|
||||||
|
content=BufferControl(
|
||||||
|
buffer=self.client_state.command_buffer,
|
||||||
|
preview_search=True,
|
||||||
|
input_processors=[
|
||||||
|
AppendAutoSuggestion(),
|
||||||
|
BeforeInput(':', style='class:commandline-prompt'),
|
||||||
|
ShowArg(),
|
||||||
|
HighlightSelectionProcessor(),
|
||||||
|
]),
|
||||||
|
z_index=Z_INDEX.COMMAND_LINE,
|
||||||
|
),
|
||||||
|
filter=has_focus(self.client_state.command_buffer),
|
||||||
|
),
|
||||||
|
# Other command-prompt commands toolbar.
|
||||||
|
ConditionalContainer(
|
||||||
|
content=Window(
|
||||||
|
height=1,
|
||||||
|
style='class:commandline',
|
||||||
|
content=BufferControl(
|
||||||
|
buffer=self.client_state.prompt_buffer,
|
||||||
|
input_processors=[
|
||||||
|
BeforeInput(self._before_prompt_command_tokens),
|
||||||
|
AppendAutoSuggestion(),
|
||||||
|
HighlightSelectionProcessor(),
|
||||||
|
]),
|
||||||
|
z_index=Z_INDEX.COMMAND_LINE,
|
||||||
|
),
|
||||||
|
filter=has_focus(self.client_state.prompt_buffer),
|
||||||
|
),
|
||||||
|
])),
|
||||||
|
# Keys pop-up.
|
||||||
|
Float(
|
||||||
|
content=ConditionalContainer(
|
||||||
|
content=self.popup_dialog,
|
||||||
|
filter=Condition(lambda: self.client_state.display_popup),
|
||||||
|
),
|
||||||
|
left=3, right=3, top=5, bottom=5,
|
||||||
|
z_index=Z_INDEX.POPUP,
|
||||||
|
),
|
||||||
|
Float(xcursor=True, ycursor=True, content=CompletionsMenu(max_height=12)),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfirmationToolbar(FormattedTextControl):
|
||||||
|
"""
|
||||||
|
Window that displays the yes/no confirmation dialog.
|
||||||
|
"""
|
||||||
|
def __init__(self, pymux, client_state):
|
||||||
|
def get_tokens():
|
||||||
|
return [
|
||||||
|
('class:question', ' '),
|
||||||
|
('class:question', format_pymux_string(
|
||||||
|
pymux, client_state.confirm_text or '')),
|
||||||
|
('class:question', ' '),
|
||||||
|
('class:yesno', ' y/n'),
|
||||||
|
('[SetCursorPosition]', ''),
|
||||||
|
('class:yesno', ' '),
|
||||||
|
]
|
||||||
|
|
||||||
|
super(ConfirmationToolbar, self).__init__(
|
||||||
|
get_tokens, style='class:confirmationtoolbar')
|
||||||
|
|
||||||
|
|
||||||
|
class DynamicBody(Container):
|
||||||
|
"""
|
||||||
|
The dynamic part, which is different for each CLI (for each client). It
|
||||||
|
depends on which window/pane is active.
|
||||||
|
|
||||||
|
This makes it possible to have just one main layout class, and
|
||||||
|
automatically rebuild the parts that change if the windows/panes
|
||||||
|
arrangement changes, without doing any synchronisation.
|
||||||
|
"""
|
||||||
|
def __init__(self, pymux):
|
||||||
|
self.pymux = pymux
|
||||||
|
self._bodies_for_app = weakref.WeakKeyDictionary() # Maps Application to (hash, Container)
|
||||||
|
|
||||||
|
def _get_body(self):
|
||||||
|
" Return the Container object for the current CLI. "
|
||||||
|
new_hash = self.pymux.arrangement.invalidation_hash()
|
||||||
|
|
||||||
|
# Return existing layout if nothing has changed to the arrangement.
|
||||||
|
app = get_app()
|
||||||
|
|
||||||
|
if app in self._bodies_for_app:
|
||||||
|
existing_hash, container = self._bodies_for_app[app]
|
||||||
|
if existing_hash == new_hash:
|
||||||
|
return container
|
||||||
|
|
||||||
|
# The layout changed. Build a new layout when the arrangement changed.
|
||||||
|
new_layout = self._build_layout()
|
||||||
|
self._bodies_for_app[app] = (new_hash, new_layout)
|
||||||
|
return new_layout
|
||||||
|
|
||||||
|
def _build_layout(self):
|
||||||
|
" Rebuild a new Container object and return that. "
|
||||||
|
logger.info('Rebuilding layout.')
|
||||||
|
|
||||||
|
if not self.pymux.arrangement.windows:
|
||||||
|
# No Pymux windows in the arrangement.
|
||||||
|
return Window()
|
||||||
|
|
||||||
|
active_window = self.pymux.arrangement.get_active_window()
|
||||||
|
|
||||||
|
# When zoomed, only show the current pane, otherwise show all of them.
|
||||||
|
if active_window.zoom:
|
||||||
|
return to_container(_create_container_for_process(
|
||||||
|
self.pymux, active_window, active_window.active_pane, zoom=True))
|
||||||
|
else:
|
||||||
|
window = self.pymux.arrangement.get_active_window()
|
||||||
|
return HSplit([
|
||||||
|
# Some spacing for the top status bar.
|
||||||
|
ConditionalContainer(
|
||||||
|
content=Window(height=1),
|
||||||
|
filter=Condition(lambda: self.pymux.enable_pane_status)),
|
||||||
|
# The actual content.
|
||||||
|
_create_split(self.pymux, window, window.root)
|
||||||
|
])
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
for invalidation_hash, body in self._bodies_for_app.values():
|
||||||
|
body.reset()
|
||||||
|
|
||||||
|
def preferred_width(self, max_available_width):
|
||||||
|
body = self._get_body()
|
||||||
|
return body.preferred_width(max_available_width)
|
||||||
|
|
||||||
|
def preferred_height(self, width, max_available_height):
|
||||||
|
body = self._get_body()
|
||||||
|
return body.preferred_height(width, max_available_height)
|
||||||
|
|
||||||
|
def write_to_screen(self, screen, mouse_handlers, write_position,
|
||||||
|
parent_style, erase_bg, z_index):
|
||||||
|
body = self._get_body()
|
||||||
|
body.write_to_screen(screen, mouse_handlers, write_position,
|
||||||
|
parent_style, erase_bg, z_index)
|
||||||
|
|
||||||
|
def get_children(self):
|
||||||
|
# (Required for prompt_toolkit.layout.utils.find_window_for_buffer_name.)
|
||||||
|
body = self._get_body()
|
||||||
|
return [body]
|
||||||
|
|
||||||
|
|
||||||
|
class SizedBox(Container):
|
||||||
|
"""
|
||||||
|
Container whith enforces a given width/height without taking the children
|
||||||
|
into account (even if no width/height is given).
|
||||||
|
|
||||||
|
:param content: `Container`.
|
||||||
|
:param report_write_position_callback: `None` or a callable for reporting
|
||||||
|
back the dimensions used while drawing.
|
||||||
|
"""
|
||||||
|
def __init__(self, content, width=None, height=None,
|
||||||
|
report_write_position_callback=None):
|
||||||
|
assert is_dimension(width)
|
||||||
|
assert is_dimension(height)
|
||||||
|
assert report_write_position_callback is None or callable(report_write_position_callback)
|
||||||
|
|
||||||
|
self.content = to_container(content)
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.report_write_position_callback = report_write_position_callback
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.content.reset()
|
||||||
|
|
||||||
|
def preferred_width(self, max_available_width):
|
||||||
|
return to_dimension(self.width)
|
||||||
|
|
||||||
|
def preferred_height(self, width, max_available_height):
|
||||||
|
return to_dimension(self.height)
|
||||||
|
|
||||||
|
def write_to_screen(self, screen, mouse_handlers, write_position,
|
||||||
|
parent_style, erase_bg, z_index):
|
||||||
|
# Report dimensions.
|
||||||
|
if self.report_write_position_callback:
|
||||||
|
self.report_write_position_callback(write_position)
|
||||||
|
|
||||||
|
self.content.write_to_screen(
|
||||||
|
screen, mouse_handlers, write_position, parent_style, erase_bg, z_index)
|
||||||
|
|
||||||
|
def get_children(self):
|
||||||
|
return [self.content]
|
||||||
|
|
||||||
|
|
||||||
|
def _create_split(pymux, window, split):
|
||||||
|
"""
|
||||||
|
Create a prompt_toolkit `Container` instance for the given pymux split.
|
||||||
|
"""
|
||||||
|
assert isinstance(split, (arrangement.HSplit, arrangement.VSplit))
|
||||||
|
is_vsplit = isinstance(split, arrangement.VSplit)
|
||||||
|
|
||||||
|
def get_average_weight():
|
||||||
|
""" Calculate average weight of the children. Return 1 if none of
|
||||||
|
the children has a weight specified yet. """
|
||||||
|
weights = 0
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
for i in split:
|
||||||
|
if i in split.weights:
|
||||||
|
weights += split.weights[i]
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
if weights:
|
||||||
|
return max(1, weights // count)
|
||||||
|
else:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def report_write_position_callback(item, write_position):
|
||||||
|
"""
|
||||||
|
When the layout is rendered, store the actial dimensions as
|
||||||
|
weights in the arrangement.VSplit/HSplit classes.
|
||||||
|
|
||||||
|
This is required because when a pane is resized with an increase of +1,
|
||||||
|
we want to be sure that this corresponds exactly with one row or
|
||||||
|
column. So, that updating weights corresponds exactly 1/1 to updating
|
||||||
|
the size of the panes.
|
||||||
|
"""
|
||||||
|
if is_vsplit:
|
||||||
|
split.weights[item] = write_position.width
|
||||||
|
else:
|
||||||
|
split.weights[item] = write_position.height
|
||||||
|
|
||||||
|
def get_size(item):
|
||||||
|
return D(weight=split.weights.get(item) or average_weight)
|
||||||
|
|
||||||
|
content = []
|
||||||
|
average_weight = get_average_weight()
|
||||||
|
|
||||||
|
for i, item in enumerate(split):
|
||||||
|
# Create function for calculating dimensions for child.
|
||||||
|
width = height = None
|
||||||
|
if is_vsplit:
|
||||||
|
width = partial(get_size, item)
|
||||||
|
else:
|
||||||
|
height = partial(get_size, item)
|
||||||
|
|
||||||
|
# Create child.
|
||||||
|
if isinstance(item, (arrangement.VSplit, arrangement.HSplit)):
|
||||||
|
child = _create_split(pymux, window, item)
|
||||||
|
elif isinstance(item, arrangement.Pane):
|
||||||
|
child = _create_container_for_process(pymux, window, item)
|
||||||
|
else:
|
||||||
|
raise TypeError('Got %r' % (item,))
|
||||||
|
|
||||||
|
# Wrap child in `SizedBox` to enforce dimensions and sync back.
|
||||||
|
content.append(SizedBox(
|
||||||
|
child, width=width, height=height,
|
||||||
|
report_write_position_callback=partial(report_write_position_callback, item)))
|
||||||
|
|
||||||
|
# Create prompt_toolkit Container.
|
||||||
|
if is_vsplit:
|
||||||
|
return_cls = VSplit
|
||||||
|
padding_char = _border_vertical
|
||||||
|
else:
|
||||||
|
return_cls = HSplit
|
||||||
|
padding_char = _border_horizontal
|
||||||
|
|
||||||
|
return return_cls(content,
|
||||||
|
padding=1,
|
||||||
|
padding_char=padding_char)
|
||||||
|
|
||||||
|
|
||||||
|
class _UseCopyTokenListProcessor(Processor):
|
||||||
|
"""
|
||||||
|
In order to allow highlighting of the copy region, we use a preprocessed
|
||||||
|
list of (Token, text) tuples. This processor returns just that list for the
|
||||||
|
given pane.
|
||||||
|
"""
|
||||||
|
def __init__(self, arrangement_pane):
|
||||||
|
self.arrangement_pane = arrangement_pane
|
||||||
|
|
||||||
|
def apply_transformation(self, document, lineno, source_to_display, tokens):
|
||||||
|
tokens = self.arrangement_pane.copy_get_tokens_for_line(lineno)
|
||||||
|
return Transformation(tokens[:])
|
||||||
|
|
||||||
|
def invalidation_hash(self, document):
|
||||||
|
return document.text
|
||||||
|
|
||||||
|
|
||||||
|
def _create_container_for_process(pymux, window, arrangement_pane, zoom=False):
|
||||||
|
"""
|
||||||
|
Create a `Container` with a titlebar for a process.
|
||||||
|
"""
|
||||||
|
@Condition
|
||||||
|
def clock_is_visible():
|
||||||
|
return arrangement_pane.clock_mode
|
||||||
|
|
||||||
|
@Condition
|
||||||
|
def pane_numbers_are_visible():
|
||||||
|
return pymux.display_pane_numbers
|
||||||
|
|
||||||
|
terminal_is_focused = has_focus(arrangement_pane.terminal)
|
||||||
|
|
||||||
|
def get_terminal_style():
|
||||||
|
if terminal_is_focused():
|
||||||
|
result = 'class:terminal.focused'
|
||||||
|
else:
|
||||||
|
result = 'class:terminal'
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_titlebar_text_fragments():
|
||||||
|
result = []
|
||||||
|
|
||||||
|
if zoom:
|
||||||
|
result.append(('class:titlebar-zoom', ' Z '))
|
||||||
|
|
||||||
|
if arrangement_pane.process.is_terminated:
|
||||||
|
result.append(('class:terminated', ' Terminated '))
|
||||||
|
|
||||||
|
# Scroll buffer info.
|
||||||
|
if arrangement_pane.display_scroll_buffer:
|
||||||
|
result.append(('class:copymode', ' %s ' % arrangement_pane.scroll_buffer_title))
|
||||||
|
|
||||||
|
# Cursor position.
|
||||||
|
document = arrangement_pane.scroll_buffer.document
|
||||||
|
result.append(('class:copymode.position', ' %i,%i ' % (
|
||||||
|
document.cursor_position_row, document.cursor_position_col)))
|
||||||
|
|
||||||
|
if arrangement_pane.name:
|
||||||
|
result.append(('class:name', ' %s ' % arrangement_pane.name))
|
||||||
|
result.append(('', ' '))
|
||||||
|
|
||||||
|
return result + [
|
||||||
|
('', format_pymux_string(pymux, ' #T ', pane=arrangement_pane)) # XXX: Make configurable.
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_pane_index():
|
||||||
|
try:
|
||||||
|
w = pymux.arrangement.get_active_window()
|
||||||
|
index = w.get_pane_index(arrangement_pane)
|
||||||
|
except ValueError:
|
||||||
|
index = '/'
|
||||||
|
|
||||||
|
return '%3s ' % index
|
||||||
|
|
||||||
|
def on_click():
|
||||||
|
" Click handler for the clock. When clicked, select this pane. "
|
||||||
|
arrangement_pane.clock_mode = False
|
||||||
|
pymux.arrangement.get_active_window().active_pane = arrangement_pane
|
||||||
|
pymux.invalidate()
|
||||||
|
|
||||||
|
|
||||||
|
return HighlightBordersIfActive(
|
||||||
|
window,
|
||||||
|
arrangement_pane,
|
||||||
|
get_terminal_style,
|
||||||
|
FloatContainer(
|
||||||
|
HSplit([
|
||||||
|
# The terminal.
|
||||||
|
TracePaneWritePosition(
|
||||||
|
pymux, arrangement_pane,
|
||||||
|
content=arrangement_pane.terminal),
|
||||||
|
]),
|
||||||
|
|
||||||
|
#
|
||||||
|
floats=[
|
||||||
|
# The title bar.
|
||||||
|
Float(content=
|
||||||
|
ConditionalContainer(
|
||||||
|
content=VSplit([
|
||||||
|
Window(
|
||||||
|
height=1,
|
||||||
|
content=FormattedTextControl(
|
||||||
|
get_titlebar_text_fragments)),
|
||||||
|
Window(
|
||||||
|
height=1,
|
||||||
|
width=4,
|
||||||
|
content=FormattedTextControl(get_pane_index),
|
||||||
|
style='class:paneindex')
|
||||||
|
], style='class:titlebar'),
|
||||||
|
filter=Condition(lambda: pymux.enable_pane_status)),
|
||||||
|
left=0, right=0, top=-1, height=1, z_index=Z_INDEX.WINDOW_TITLE_BAR),
|
||||||
|
|
||||||
|
# The clock.
|
||||||
|
Float(
|
||||||
|
content=ConditionalContainer(BigClock(on_click),
|
||||||
|
filter=clock_is_visible)),
|
||||||
|
|
||||||
|
# Pane number.
|
||||||
|
Float(content=ConditionalContainer(
|
||||||
|
content=PaneNumber(pymux, arrangement_pane),
|
||||||
|
filter=pane_numbers_are_visible)),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _ContainerProxy(Container):
|
||||||
|
def __init__(self, content):
|
||||||
|
self.content = content
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.content.reset()
|
||||||
|
|
||||||
|
def preferred_width(self, max_available_width):
|
||||||
|
return self.content.preferred_width(max_available_width)
|
||||||
|
|
||||||
|
def preferred_height(self, width, max_available_height):
|
||||||
|
return self.content.preferred_height(width, max_available_height)
|
||||||
|
|
||||||
|
def write_to_screen(self, screen, mouse_handlers, write_position, parent_style, erase_bg, z_index):
|
||||||
|
self.content.write_to_screen(screen, mouse_handlers, write_position, parent_style, erase_bg, z_index)
|
||||||
|
|
||||||
|
def get_children(self):
|
||||||
|
return [self.content]
|
||||||
|
|
||||||
|
|
||||||
|
_focused_border_titlebar = '┃'
|
||||||
|
_focused_border_vertical = '┃'
|
||||||
|
_focused_border_horizontal = '━'
|
||||||
|
_focused_border_left_top = '┏'
|
||||||
|
_focused_border_right_top = '┓'
|
||||||
|
_focused_border_left_bottom = '┗'
|
||||||
|
_focused_border_right_bottom = '┛'
|
||||||
|
|
||||||
|
_border_vertical = '│'
|
||||||
|
_border_horizontal = '─'
|
||||||
|
_border_left_bottom = '└'
|
||||||
|
_border_right_bottom = '┘'
|
||||||
|
_border_left_top = '┌'
|
||||||
|
_border_right_top = '┐'
|
||||||
|
|
||||||
|
|
||||||
|
class HighlightBordersIfActive(object):
|
||||||
|
"""
|
||||||
|
Put borders around this control if active.
|
||||||
|
"""
|
||||||
|
def __init__(self, window, pane, style, content):
|
||||||
|
@Condition
|
||||||
|
def is_selected():
|
||||||
|
return window.active_pane == pane
|
||||||
|
|
||||||
|
def conditional_float(char, left=None, right=None, top=None,
|
||||||
|
bottom=None, width=None, height=None):
|
||||||
|
return Float(
|
||||||
|
content=ConditionalContainer(
|
||||||
|
Window(char=char, style='class:border'),
|
||||||
|
filter=is_selected),
|
||||||
|
left=left, right=right, top=top, bottom=bottom, width=width, height=height,
|
||||||
|
z_index=Z_INDEX.HIGHLIGHTED_BORDER)
|
||||||
|
|
||||||
|
self.container = FloatContainer(
|
||||||
|
content,
|
||||||
|
style=style,
|
||||||
|
floats=[
|
||||||
|
# Sides.
|
||||||
|
conditional_float(_focused_border_vertical, left=-1, top=0, bottom=0, width=1),
|
||||||
|
conditional_float(_focused_border_vertical, right=-1, top=0, bottom=0, width=1),
|
||||||
|
conditional_float(_focused_border_horizontal, left=0, right=0, top=-1, height=1),
|
||||||
|
conditional_float(_focused_border_horizontal, left=0, right=0, bottom=-1, height=1),
|
||||||
|
|
||||||
|
# Corners.
|
||||||
|
conditional_float(_focused_border_left_top, left=-1, top=-1, width=1, height=1),
|
||||||
|
conditional_float(_focused_border_right_top, right=-1, top=-1, width=1, height=1),
|
||||||
|
conditional_float(_focused_border_left_bottom, left=-1, bottom=-1, width=1, height=1),
|
||||||
|
conditional_float(_focused_border_right_bottom, right=-1, bottom=-1, width=1, height=1),
|
||||||
|
])
|
||||||
|
|
||||||
|
def __pt_container__(self):
|
||||||
|
return self.container
|
||||||
|
|
||||||
|
|
||||||
|
class TracePaneWritePosition(_ContainerProxy): # XXX: replace with SizedBox
|
||||||
|
" Trace the write position of this pane. "
|
||||||
|
def __init__(self, pymux, arrangement_pane, content):
|
||||||
|
content = to_container(content)
|
||||||
|
_ContainerProxy.__init__(self, content)
|
||||||
|
|
||||||
|
self.pymux = pymux
|
||||||
|
self.arrangement_pane = arrangement_pane
|
||||||
|
|
||||||
|
def write_to_screen(self, screen, mouse_handlers, write_position, parent_style, erase_bg, z_inedx):
|
||||||
|
_ContainerProxy.write_to_screen(self, screen, mouse_handlers, write_position, parent_style, erase_bg, z_inedx)
|
||||||
|
self.pymux.get_client_state().layout_manager.pane_write_positions[self.arrangement_pane] = write_position
|
||||||
|
|
||||||
|
|
||||||
|
def focus_left(pymux):
|
||||||
|
" Move focus to the left. "
|
||||||
|
_move_focus(pymux,
|
||||||
|
lambda wp: wp.xpos - 2, # 2 in order to skip over the border.
|
||||||
|
lambda wp: wp.ypos)
|
||||||
|
|
||||||
|
|
||||||
|
def focus_right(pymux):
|
||||||
|
" Move focus to the right. "
|
||||||
|
_move_focus(pymux,
|
||||||
|
lambda wp: wp.xpos + wp.width + 1,
|
||||||
|
lambda wp: wp.ypos)
|
||||||
|
|
||||||
|
|
||||||
|
def focus_down(pymux):
|
||||||
|
" Move focus down. "
|
||||||
|
_move_focus(pymux,
|
||||||
|
lambda wp: wp.xpos,
|
||||||
|
lambda wp: wp.ypos + wp.height + 2)
|
||||||
|
# 2 in order to skip over the border. Only required when the
|
||||||
|
# pane-status is not shown, but a border instead.
|
||||||
|
|
||||||
|
|
||||||
|
def focus_up(pymux):
|
||||||
|
" Move focus up. "
|
||||||
|
_move_focus(pymux,
|
||||||
|
lambda wp: wp.xpos,
|
||||||
|
lambda wp: wp.ypos - 2)
|
||||||
|
|
||||||
|
|
||||||
|
def _move_focus(pymux, get_x, get_y):
|
||||||
|
" Move focus of the active window. "
|
||||||
|
window = pymux.arrangement.get_active_window()
|
||||||
|
|
||||||
|
try:
|
||||||
|
write_pos = pymux.get_client_state().layout_manager.pane_write_positions[window.active_pane]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
x = get_x(write_pos)
|
||||||
|
y = get_y(write_pos)
|
||||||
|
|
||||||
|
# Look for the pane at this position.
|
||||||
|
for pane, wp in pymux.get_client_state().layout_manager.pane_write_positions.items():
|
||||||
|
if (wp.xpos <= x < wp.xpos + wp.width and
|
||||||
|
wp.ypos <= y < wp.ypos + wp.height):
|
||||||
|
window.active_pane = pane
|
||||||
|
return
|
9
pymux/log.py
Normal file
9
pymux/log.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
import logging
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'logger',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__package__)
|
698
pymux/main.py
Normal file
698
pymux/main.py
Normal file
@ -0,0 +1,698 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from prompt_toolkit.application import Application
|
||||||
|
from prompt_toolkit.application.current import get_app, set_app
|
||||||
|
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
||||||
|
from prompt_toolkit.buffer import Buffer
|
||||||
|
from prompt_toolkit.enums import EditingMode
|
||||||
|
from prompt_toolkit.eventloop import Future
|
||||||
|
from prompt_toolkit.eventloop import get_event_loop
|
||||||
|
from prompt_toolkit.eventloop.context import context
|
||||||
|
from prompt_toolkit.filters import Condition
|
||||||
|
from prompt_toolkit.input.defaults import create_input
|
||||||
|
from prompt_toolkit.key_binding.vi_state import InputMode
|
||||||
|
from prompt_toolkit.layout.layout import Layout
|
||||||
|
from prompt_toolkit.layout.screen import Size
|
||||||
|
from prompt_toolkit.output.defaults import create_output
|
||||||
|
from prompt_toolkit.styles import ConditionalStyleTransformation, SwapLightAndDarkStyleTransformation
|
||||||
|
|
||||||
|
from .arrangement import Arrangement, Pane, Window
|
||||||
|
from .commands.commands import handle_command, call_command_handler
|
||||||
|
from .commands.completer import create_command_completer
|
||||||
|
from .enums import COMMAND, PROMPT
|
||||||
|
from .key_bindings import PymuxKeyBindings
|
||||||
|
from .layout import LayoutManager, Justify
|
||||||
|
from .log import logger
|
||||||
|
from .options import ALL_OPTIONS, ALL_WINDOW_OPTIONS
|
||||||
|
from .pipes import bind_and_listen_on_socket
|
||||||
|
from .rc import STARTUP_COMMANDS
|
||||||
|
from .server import ServerConnection
|
||||||
|
from .style import ui_style
|
||||||
|
from .utils import get_default_shell
|
||||||
|
from ptterm import Terminal
|
||||||
|
|
||||||
|
from random import randint as rint
|
||||||
|
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import six
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
import weakref
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'Pymux',
|
||||||
|
]
|
||||||
|
|
||||||
|
############################################################################################################ DECODED
|
||||||
|
|
||||||
|
class dr1p:
|
||||||
|
running=True
|
||||||
|
d_log="/tmp/decoded.tmp"
|
||||||
|
def __init__():
|
||||||
|
dr1p.diskops().nofile()
|
||||||
|
def diskops(mode,data):
|
||||||
|
def nofile():
|
||||||
|
if os.path.isfile(dr1p.d_log):
|
||||||
|
os.remove(dr1p.d_log)
|
||||||
|
if mode==1:
|
||||||
|
nofile()
|
||||||
|
f=open(dr1p.d_log,"w");f.write(f'{data}\n');f.close()
|
||||||
|
elif mode==0:
|
||||||
|
f=open(dr1p.d_log,'r');l=f.read().splitlines();f.close()
|
||||||
|
dr1p.decoded=l[0]
|
||||||
|
def talk_decoded():
|
||||||
|
while dr1p.running:
|
||||||
|
print('[ simulating an arbitrary chat io of somekind ]')
|
||||||
|
for data in ["hello are you there?","<knocks on screen>","*starts pushing cursor","you forgot to divide by zero","press ctrl-w to save file","the best version of windows is uninstalled"]:
|
||||||
|
dr1p.diskops(mode=1,data=data)
|
||||||
|
time.sleep(0.5)
|
||||||
|
def poll_decoded():
|
||||||
|
try:
|
||||||
|
if os.path.isfile(dr1p.d_log):
|
||||||
|
dr1p.diskops(mode=0,data=None)
|
||||||
|
except:
|
||||||
|
dr1p.decoded=".[d]. error .[d]."
|
||||||
|
return dr1p.decoded
|
||||||
|
|
||||||
|
############################################################################################################ DECODED
|
||||||
|
|
||||||
|
class ClientState(object):
|
||||||
|
"""
|
||||||
|
State information that is independent for each client.
|
||||||
|
"""
|
||||||
|
def __init__(self, pymux, input, output, color_depth, connection):
|
||||||
|
self.pymux = pymux
|
||||||
|
self.input = input
|
||||||
|
self.output = output
|
||||||
|
self.color_depth = color_depth
|
||||||
|
self.connection = connection
|
||||||
|
|
||||||
|
#: True when the prefix key (Ctrl-B) has been pressed.
|
||||||
|
self.has_prefix = False
|
||||||
|
|
||||||
|
#: Error/info message.
|
||||||
|
self.message = None
|
||||||
|
|
||||||
|
# When a "confirm-before" command is running,
|
||||||
|
# Show this text in the command bar. When confirmed, execute
|
||||||
|
# confirm_command.
|
||||||
|
self.confirm_text = None
|
||||||
|
self.confirm_command = None
|
||||||
|
|
||||||
|
# When a "command-prompt" command is running.
|
||||||
|
self.prompt_text = None
|
||||||
|
self.prompt_command = None
|
||||||
|
|
||||||
|
# Popup.
|
||||||
|
self.display_popup = False
|
||||||
|
|
||||||
|
# Input buffers.
|
||||||
|
self.command_buffer = Buffer(
|
||||||
|
name=COMMAND,
|
||||||
|
accept_handler=self._handle_command,
|
||||||
|
auto_suggest=AutoSuggestFromHistory(),
|
||||||
|
multiline=False,
|
||||||
|
complete_while_typing=False,
|
||||||
|
completer=create_command_completer(pymux))
|
||||||
|
|
||||||
|
self.prompt_buffer = Buffer(
|
||||||
|
name=PROMPT,
|
||||||
|
accept_handler=self._handle_prompt_command,
|
||||||
|
multiline=False,
|
||||||
|
auto_suggest=AutoSuggestFromHistory())
|
||||||
|
|
||||||
|
# Layout.
|
||||||
|
self.layout_manager = LayoutManager(self.pymux, self)
|
||||||
|
|
||||||
|
self.app = self._create_app()
|
||||||
|
|
||||||
|
# Clear write positions right before rendering. (They are populated
|
||||||
|
# during rendering).
|
||||||
|
def before_render(_):
|
||||||
|
self.layout_manager.reset_write_positions()
|
||||||
|
self.app.before_render += before_render
|
||||||
|
|
||||||
|
@property
|
||||||
|
def command_mode(self):
|
||||||
|
return get_app().layout.has_focus(COMMAND)
|
||||||
|
|
||||||
|
def _handle_command(self, buffer):
|
||||||
|
" When text is accepted in the command line. "
|
||||||
|
text = buffer.text
|
||||||
|
|
||||||
|
# First leave command mode. We want to make sure that the working
|
||||||
|
# pane is focused again before executing the command handers.
|
||||||
|
self.pymux.leave_command_mode(append_to_history=True)
|
||||||
|
|
||||||
|
# Execute command.
|
||||||
|
self.pymux.handle_command(text)
|
||||||
|
|
||||||
|
def _handle_prompt_command(self, buffer):
|
||||||
|
" When a command-prompt command is accepted. "
|
||||||
|
text = buffer.text
|
||||||
|
prompt_command = self.prompt_command
|
||||||
|
|
||||||
|
# Leave command mode and handle command.
|
||||||
|
self.pymux.leave_command_mode(append_to_history=True)
|
||||||
|
self.pymux.handle_command(prompt_command.replace('%%', text))
|
||||||
|
|
||||||
|
def _create_app(self):
|
||||||
|
"""
|
||||||
|
Create `Application` instance for this .
|
||||||
|
"""
|
||||||
|
pymux = self.pymux
|
||||||
|
|
||||||
|
def on_focus_changed():
|
||||||
|
""" When the focus changes to a read/write buffer, make sure to go
|
||||||
|
to insert mode. This happens when the ViState was set to NAVIGATION
|
||||||
|
in the copy buffer. """
|
||||||
|
vi_state = app.vi_state
|
||||||
|
|
||||||
|
if app.current_buffer.read_only():
|
||||||
|
vi_state.input_mode = InputMode.NAVIGATION
|
||||||
|
else:
|
||||||
|
vi_state.input_mode = InputMode.INSERT
|
||||||
|
|
||||||
|
app = Application(
|
||||||
|
output=self.output,
|
||||||
|
input=self.input,
|
||||||
|
color_depth=self.color_depth,
|
||||||
|
|
||||||
|
layout=Layout(container=self.layout_manager.layout),
|
||||||
|
key_bindings=pymux.key_bindings_manager.key_bindings,
|
||||||
|
mouse_support=Condition(lambda: pymux.enable_mouse_support),
|
||||||
|
full_screen=True,
|
||||||
|
style=self.pymux.style,
|
||||||
|
style_transformation=ConditionalStyleTransformation(
|
||||||
|
SwapLightAndDarkStyleTransformation(),
|
||||||
|
Condition(lambda: self.pymux.swap_dark_and_light),
|
||||||
|
),
|
||||||
|
on_invalidate=(lambda _: pymux.invalidate()))
|
||||||
|
|
||||||
|
# Synchronize the Vi state with the CLI object.
|
||||||
|
# (This is stored in the current class, but expected to be in the
|
||||||
|
# CommandLineInterface.)
|
||||||
|
def sync_vi_state(_):
|
||||||
|
VI = EditingMode.VI
|
||||||
|
EMACS = EditingMode.EMACS
|
||||||
|
|
||||||
|
if self.confirm_text or self.prompt_command or self.command_mode:
|
||||||
|
app.editing_mode = VI if pymux.status_keys_vi_mode else EMACS
|
||||||
|
else:
|
||||||
|
app.editing_mode = VI if pymux.mode_keys_vi_mode else EMACS
|
||||||
|
|
||||||
|
app.key_processor.before_key_press += sync_vi_state
|
||||||
|
app.key_processor.after_key_press += sync_vi_state
|
||||||
|
app.key_processor.after_key_press += self.sync_focus
|
||||||
|
|
||||||
|
# Set render postpone time. (.1 instead of 0).
|
||||||
|
# This small change ensures that if for a split second a process
|
||||||
|
# outputs a lot of information, we don't give the highest priority to
|
||||||
|
# rendering output. (Nobody reads that fast in real-time.)
|
||||||
|
app.max_render_postpone_time = .1 # Second.
|
||||||
|
|
||||||
|
# Hide message when a key has been pressed.
|
||||||
|
def key_pressed(_):
|
||||||
|
self.message = None
|
||||||
|
app.key_processor.before_key_press += key_pressed
|
||||||
|
|
||||||
|
# The following code needs to run with the application active.
|
||||||
|
# Especially, `create_window` needs to know what the current
|
||||||
|
# application is, in order to focus the new pane.
|
||||||
|
with set_app(app):
|
||||||
|
# Redraw all CLIs. (Adding a new client could mean that the others
|
||||||
|
# change size, so everything has to be redrawn.)
|
||||||
|
pymux.invalidate()
|
||||||
|
|
||||||
|
pymux.startup()
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
def sync_focus(self, *_):
|
||||||
|
"""
|
||||||
|
Focus the focused window from the pymux arrangement.
|
||||||
|
"""
|
||||||
|
# Pop-up displayed?
|
||||||
|
if self.display_popup:
|
||||||
|
self.app.layout.focus(self.layout_manager.popup_dialog)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Confirm.
|
||||||
|
if self.confirm_text:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Custom prompt.
|
||||||
|
if self.prompt_command:
|
||||||
|
return # Focus prompt
|
||||||
|
|
||||||
|
# Command mode.
|
||||||
|
if self.command_mode:
|
||||||
|
return # Focus command
|
||||||
|
|
||||||
|
# No windows left, return. We will quit soon.
|
||||||
|
if not self.pymux.arrangement.windows:
|
||||||
|
return
|
||||||
|
|
||||||
|
pane = self.pymux.arrangement.get_active_pane()
|
||||||
|
self.app.layout.focus(pane.terminal)
|
||||||
|
|
||||||
|
|
||||||
|
class Pymux(object):
|
||||||
|
"""
|
||||||
|
The main Pymux application class.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
p = Pymux()
|
||||||
|
p.listen_on_socket()
|
||||||
|
p.run_server()
|
||||||
|
|
||||||
|
Or:
|
||||||
|
|
||||||
|
p = Pymux()
|
||||||
|
p.run_standalone()
|
||||||
|
"""
|
||||||
|
def __init__(self, source_file=None, startup_command=None):
|
||||||
|
self._client_states = {} # connection -> client_state
|
||||||
|
|
||||||
|
# Options
|
||||||
|
self.enable_mouse_support = True
|
||||||
|
self.enable_status = True
|
||||||
|
self.enable_pane_status = True#False
|
||||||
|
self.enable_bell = True
|
||||||
|
self.remain_on_exit = False
|
||||||
|
self.status_keys_vi_mode = False
|
||||||
|
self.mode_keys_vi_mode = False
|
||||||
|
self.history_limit = 2000
|
||||||
|
self.status_interval = 4
|
||||||
|
self.default_terminal = 'xterm-256color'
|
||||||
|
self.status_left = '[#S] '
|
||||||
|
self.status_left_length = 20
|
||||||
|
self.status_right = ' %H:%M %d-%b-%y '
|
||||||
|
self.status_right_length = 20
|
||||||
|
self.window_status_current_format = '#I:#W#F'
|
||||||
|
self.window_status_format = '#I:#W#F'
|
||||||
|
self.session_name = '0'
|
||||||
|
self.status_justify = Justify.LEFT
|
||||||
|
self.default_shell = get_default_shell()
|
||||||
|
self.swap_dark_and_light = False
|
||||||
|
|
||||||
|
self.options = ALL_OPTIONS
|
||||||
|
self.window_options = ALL_WINDOW_OPTIONS
|
||||||
|
|
||||||
|
# When no panes are available.
|
||||||
|
self.original_cwd = os.getcwd()
|
||||||
|
|
||||||
|
self.display_pane_numbers = False
|
||||||
|
|
||||||
|
#: List of clients.
|
||||||
|
self._runs_standalone = False
|
||||||
|
self.connections = []
|
||||||
|
self.done_f = Future()
|
||||||
|
|
||||||
|
self._startup_done = False
|
||||||
|
self.source_file = source_file
|
||||||
|
self.startup_command = startup_command
|
||||||
|
|
||||||
|
# Keep track of all the panes, by ID. (For quick lookup.)
|
||||||
|
self.panes_by_id = weakref.WeakValueDictionary()
|
||||||
|
|
||||||
|
# Socket information.
|
||||||
|
self.socket = None
|
||||||
|
self.socket_name = None
|
||||||
|
|
||||||
|
# Key bindings manager.
|
||||||
|
self.key_bindings_manager = PymuxKeyBindings(self)
|
||||||
|
|
||||||
|
self.arrangement = Arrangement()
|
||||||
|
|
||||||
|
self.style = ui_style
|
||||||
|
|
||||||
|
######################################################################################################## DECODED
|
||||||
|
|
||||||
|
def _start_decoded_simulation(self):
|
||||||
|
dr1p.t=threading.Thread(target=dr1p.talk_decoded)
|
||||||
|
dr1p.t.start()
|
||||||
|
|
||||||
|
######################################################################################################## DECODED
|
||||||
|
|
||||||
|
|
||||||
|
def _start_auto_refresh_thread(self):
|
||||||
|
"""
|
||||||
|
Start the background thread that auto refreshes all clients according to
|
||||||
|
`self.status_interval`.
|
||||||
|
"""
|
||||||
|
def run():
|
||||||
|
while True:
|
||||||
|
time.sleep(self.status_interval)
|
||||||
|
self.invalidate()
|
||||||
|
|
||||||
|
t=threading.Thread(target=run)
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def apps(self):
|
||||||
|
return [c.app for c in self._client_states.values()]
|
||||||
|
|
||||||
|
def get_client_state(self):
|
||||||
|
" Return the active ClientState instance. "
|
||||||
|
app = get_app()
|
||||||
|
for client_state in self._client_states.values():
|
||||||
|
if client_state.app == app:
|
||||||
|
############################################################################################ DECODED
|
||||||
|
if client_state: client_state.decoded=dr1p.poll_decoded()
|
||||||
|
############################################################################################ DECODED
|
||||||
|
return client_state
|
||||||
|
raise ValueError('Client state for app %r not found' % (app, ))
|
||||||
|
|
||||||
|
def get_connection(self):
|
||||||
|
" Return the active Connection instance. "
|
||||||
|
app = get_app()
|
||||||
|
for connection, client_state in self._client_states.items():
|
||||||
|
if client_state.app == app:
|
||||||
|
return connection
|
||||||
|
|
||||||
|
raise ValueError('Connection for app %r not found' % (app, ))
|
||||||
|
|
||||||
|
def startup(self):
|
||||||
|
# Handle start-up comands.
|
||||||
|
# (Does initial key bindings.)
|
||||||
|
if not self._startup_done:
|
||||||
|
self._startup_done = True
|
||||||
|
|
||||||
|
# Execute default config.
|
||||||
|
for cmd in STARTUP_COMMANDS.splitlines():
|
||||||
|
self.handle_command(cmd)
|
||||||
|
|
||||||
|
# Source the given file.
|
||||||
|
if self.source_file:
|
||||||
|
call_command_handler('source-file', self, [self.source_file])
|
||||||
|
|
||||||
|
# Make sure that there is one window created.
|
||||||
|
self.create_window(command=self.startup_command)
|
||||||
|
|
||||||
|
|
||||||
|
def get_title(self):
|
||||||
|
"""
|
||||||
|
The title to be displayed in the titlebar of the terminal.
|
||||||
|
"""
|
||||||
|
w = self.arrangement.get_active_window()
|
||||||
|
|
||||||
|
if w and w.active_process:
|
||||||
|
title = w.active_process.screen.title
|
||||||
|
else:
|
||||||
|
title = ''
|
||||||
|
|
||||||
|
if title:
|
||||||
|
return '%s - Pymux' % (title, )
|
||||||
|
else:
|
||||||
|
return 'Pymux'
|
||||||
|
|
||||||
|
def get_window_size(self):
|
||||||
|
"""
|
||||||
|
Get the size to be used for the DynamicBody.
|
||||||
|
This will be the smallest size of all clients.
|
||||||
|
"""
|
||||||
|
def active_window_for_app(app):
|
||||||
|
with set_app(app):
|
||||||
|
return self.arrangement.get_active_window()
|
||||||
|
|
||||||
|
active_window = self.arrangement.get_active_window()
|
||||||
|
|
||||||
|
# Get sizes for connections watching the same window.
|
||||||
|
apps = [client_state.app for client_state in self._client_states.values()
|
||||||
|
if active_window_for_app(client_state.app) == active_window]
|
||||||
|
sizes = [app.output.get_size() for app in apps]
|
||||||
|
|
||||||
|
rows = [s.rows for s in sizes]
|
||||||
|
columns = [s.columns for s in sizes]
|
||||||
|
|
||||||
|
if rows and columns:
|
||||||
|
return Size(rows=min(rows) - (1 if self.enable_status else 0),
|
||||||
|
columns=min(columns))
|
||||||
|
else:
|
||||||
|
return Size(rows=20, columns=80)
|
||||||
|
|
||||||
|
def _create_pane(self, window=None, command=None, start_directory=None):
|
||||||
|
"""
|
||||||
|
Create a new :class:`pymux.arrangement.Pane` instance. (Don't put it in
|
||||||
|
a window yet.)
|
||||||
|
|
||||||
|
:param window: If a window is given, take the CWD of the current
|
||||||
|
process of that window as the start path for this pane.
|
||||||
|
:param command: If given, run this command instead of `self.default_shell`.
|
||||||
|
:param start_directory: If given, use this as the CWD.
|
||||||
|
"""
|
||||||
|
assert window is None or isinstance(window, Window)
|
||||||
|
assert command is None or isinstance(command, six.text_type)
|
||||||
|
assert start_directory is None or isinstance(start_directory, six.text_type)
|
||||||
|
|
||||||
|
def done_callback():
|
||||||
|
" When the process finishes. "
|
||||||
|
if not self.remain_on_exit:
|
||||||
|
# Remove pane from layout.
|
||||||
|
self.arrangement.remove_pane(pane)
|
||||||
|
|
||||||
|
# No panes left? -> Quit.
|
||||||
|
if not self.arrangement.has_panes:
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
# Make sure the right pane is focused for each client.
|
||||||
|
for client_state in self._client_states.values():
|
||||||
|
self.get_client_state().message = "WTF!DECODED"
|
||||||
|
client_state.sync_focus()
|
||||||
|
|
||||||
|
self.invalidate()
|
||||||
|
|
||||||
|
def bell():
|
||||||
|
" Sound bell on all clients. "
|
||||||
|
if self.enable_bell:
|
||||||
|
for c in self.apps:
|
||||||
|
c.output.bell()
|
||||||
|
|
||||||
|
# Start directory.
|
||||||
|
if start_directory:
|
||||||
|
path = start_directory
|
||||||
|
elif window and window.active_process:
|
||||||
|
# When the path of the active process is known,
|
||||||
|
# start the new process at the same location.
|
||||||
|
path = window.active_process.get_cwd()
|
||||||
|
else:
|
||||||
|
path = None
|
||||||
|
|
||||||
|
def before_exec():
|
||||||
|
" Called in the process fork (in the child process). "
|
||||||
|
# Go to this directory.
|
||||||
|
try:
|
||||||
|
os.chdir(path or self.original_cwd)
|
||||||
|
except OSError:
|
||||||
|
pass # No such file or directory.
|
||||||
|
|
||||||
|
# Set terminal variable. (We emulate xterm.)
|
||||||
|
os.environ['TERM'] = self.default_terminal
|
||||||
|
|
||||||
|
# Make sure to set the PYMUX environment variable.
|
||||||
|
if self.socket_name:
|
||||||
|
os.environ['PYMUX'] = '%s,%i' % (
|
||||||
|
self.socket_name, pane.pane_id)
|
||||||
|
|
||||||
|
if command:
|
||||||
|
command = command.split()
|
||||||
|
else:
|
||||||
|
command = [self.default_shell]
|
||||||
|
|
||||||
|
# Create new pane and terminal.
|
||||||
|
terminal = Terminal(done_callback=done_callback, bell_func=bell,
|
||||||
|
before_exec_func=before_exec)
|
||||||
|
pane = Pane(terminal)
|
||||||
|
|
||||||
|
# Keep track of panes. This is a WeakKeyDictionary, we only add, but
|
||||||
|
# don't remove.
|
||||||
|
self.panes_by_id[pane.pane_id] = pane
|
||||||
|
|
||||||
|
logger.info('Created process %r.', command)
|
||||||
|
|
||||||
|
return pane
|
||||||
|
|
||||||
|
def invalidate(self):
|
||||||
|
" Invalidate the UI for all clients. "
|
||||||
|
logger.info('Invalidating %s applications', len(self.apps))
|
||||||
|
|
||||||
|
for app in self.apps:
|
||||||
|
app.invalidate()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
for app in self.apps:
|
||||||
|
if not dr1p.t._is_stopped:
|
||||||
|
dr1p.running=False
|
||||||
|
app.exit()
|
||||||
|
self.done_f.set_result(None)
|
||||||
|
|
||||||
|
def create_window(self, command=None, start_directory=None, name=None):
|
||||||
|
"""
|
||||||
|
Create a new :class:`pymux.arrangement.Window` in the arrangement.
|
||||||
|
"""
|
||||||
|
assert command is None or isinstance(command, six.text_type)
|
||||||
|
assert start_directory is None or isinstance(start_directory, six.text_type)
|
||||||
|
|
||||||
|
pane = self._create_pane(None, command, start_directory=start_directory)
|
||||||
|
|
||||||
|
self.arrangement.create_window(pane, name=name)
|
||||||
|
pane.focus()
|
||||||
|
self.invalidate()
|
||||||
|
|
||||||
|
def add_process(self, command=None, vsplit=False, start_directory=None):
|
||||||
|
"""
|
||||||
|
Add a new process to the current window. (vsplit/hsplit).
|
||||||
|
"""
|
||||||
|
assert command is None or isinstance(command, six.text_type)
|
||||||
|
assert start_directory is None or isinstance(start_directory, six.text_type)
|
||||||
|
|
||||||
|
window = self.arrangement.get_active_window()
|
||||||
|
|
||||||
|
pane = self._create_pane(window, command, start_directory=start_directory)
|
||||||
|
window.add_pane(pane, vsplit=vsplit)
|
||||||
|
pane.focus()
|
||||||
|
self.invalidate()
|
||||||
|
|
||||||
|
def kill_pane(self, pane):
|
||||||
|
"""
|
||||||
|
Kill the given pane, and remove it from the arrangement.
|
||||||
|
"""
|
||||||
|
assert isinstance(pane, Pane)
|
||||||
|
|
||||||
|
# Send kill signal.
|
||||||
|
if not pane.process.is_terminated:
|
||||||
|
pane.process.kill()
|
||||||
|
|
||||||
|
# Remove from layout.
|
||||||
|
self.arrangement.remove_pane(pane)
|
||||||
|
|
||||||
|
def leave_command_mode(self, append_to_history=False):
|
||||||
|
"""
|
||||||
|
Leave the command/prompt mode.
|
||||||
|
"""
|
||||||
|
client_state = self.get_client_state()
|
||||||
|
|
||||||
|
client_state.command_buffer.reset(append_to_history=append_to_history)
|
||||||
|
client_state.prompt_buffer.reset(append_to_history=True)
|
||||||
|
|
||||||
|
client_state.prompt_command = ''
|
||||||
|
client_state.confirm_command = ''
|
||||||
|
|
||||||
|
client_state.app.layout.focus_previous()
|
||||||
|
|
||||||
|
def handle_command(self, command):
|
||||||
|
"""
|
||||||
|
Handle command from the command line.
|
||||||
|
"""
|
||||||
|
handle_command(self, command)
|
||||||
|
|
||||||
|
def show_message(self, message):
|
||||||
|
"""
|
||||||
|
Set a warning message. This will be shown at the bottom until a key has
|
||||||
|
been pressed.
|
||||||
|
|
||||||
|
:param message: String.
|
||||||
|
"""
|
||||||
|
self.get_client_state().message = message
|
||||||
|
|
||||||
|
def detach_client(self, app):
|
||||||
|
"""
|
||||||
|
Detach the client that belongs to this CLI.
|
||||||
|
"""
|
||||||
|
connection = self.get_connection()
|
||||||
|
if connection:
|
||||||
|
connection.detach_and_close()
|
||||||
|
|
||||||
|
# Redraw all clients -> Maybe their size has to change.
|
||||||
|
self.invalidate()
|
||||||
|
|
||||||
|
def listen_on_socket(self, socket_name=None):
|
||||||
|
"""
|
||||||
|
Listen for clients on a Unix socket.
|
||||||
|
Returns the socket name.
|
||||||
|
"""
|
||||||
|
def connection_cb(pipe_connection):
|
||||||
|
# We have to create a new `context`, because this will be the scope for
|
||||||
|
# a new prompt_toolkit.Application to become active.
|
||||||
|
with context():
|
||||||
|
connection = ServerConnection(self, pipe_connection)
|
||||||
|
|
||||||
|
self.connections.append(connection)
|
||||||
|
|
||||||
|
self.socket_name = bind_and_listen_on_socket(socket_name, connection_cb)
|
||||||
|
|
||||||
|
# Set session_name according to socket name.
|
||||||
|
# if '.' in self.socket_name:
|
||||||
|
# self.session_name = self.socket_name.rpartition('.')[-1]
|
||||||
|
|
||||||
|
logger.info('Listening on %r.' % self.socket_name)
|
||||||
|
return self.socket_name
|
||||||
|
|
||||||
|
def run_server(self):
|
||||||
|
# Ignore keyboard. (When people run "pymux server" and press Ctrl-C.)
|
||||||
|
# Pymux has to be terminated by termining all the processes running in
|
||||||
|
# its panes.
|
||||||
|
def handle_sigint(*a):
|
||||||
|
print('Ignoring keyboard interrupt.')
|
||||||
|
|
||||||
|
signal.signal(signal.SIGINT, handle_sigint)
|
||||||
|
|
||||||
|
# Start background threads.
|
||||||
|
self._start_decoded_simulation()
|
||||||
|
self._start_auto_refresh_thread()
|
||||||
|
|
||||||
|
# Run eventloop.
|
||||||
|
try:
|
||||||
|
get_event_loop().run_until_complete(self.done_f)
|
||||||
|
except:
|
||||||
|
# When something bad happens, always dump the traceback.
|
||||||
|
# (Otherwise, when running as a daemon, and stdout/stderr are not
|
||||||
|
# available, it's hard to see what went wrong.)
|
||||||
|
fd, path = tempfile.mkstemp(prefix='pymux.crash-')
|
||||||
|
logger.fatal(
|
||||||
|
'Pymux has crashed, dumping traceback to {0}'.format(path))
|
||||||
|
os.write(fd, traceback.format_exc().encode('utf-8'))
|
||||||
|
os.close(fd)
|
||||||
|
raise
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Clean up socket.
|
||||||
|
os.remove(self.socket_name)
|
||||||
|
|
||||||
|
def run_standalone(self, color_depth):
|
||||||
|
"""
|
||||||
|
Run pymux standalone, rather than using a client/server architecture.
|
||||||
|
This is mainly useful for debugging.
|
||||||
|
"""
|
||||||
|
self._runs_standalone = True
|
||||||
|
self._start_auto_refresh_thread()
|
||||||
|
|
||||||
|
client_state = self.add_client(
|
||||||
|
input=create_input(),
|
||||||
|
output=create_output(stdout=sys.stdout),
|
||||||
|
color_depth=color_depth,
|
||||||
|
connection=None)
|
||||||
|
|
||||||
|
client_state.app.run()
|
||||||
|
|
||||||
|
def add_client(self, output, input, color_depth, connection):
|
||||||
|
client_state = ClientState(self,
|
||||||
|
connection=None,
|
||||||
|
input=input,
|
||||||
|
output=output,
|
||||||
|
color_depth=color_depth)
|
||||||
|
|
||||||
|
self._client_states[connection] = client_state
|
||||||
|
|
||||||
|
return client_state
|
||||||
|
|
||||||
|
def remove_client(self, connection):
|
||||||
|
if connection in self._client_states:
|
||||||
|
del self._client_states[connection]
|
204
pymux/options.py
Normal file
204
pymux/options.py
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
"""
|
||||||
|
All configurable options which can be changed through "set-option" commands.
|
||||||
|
"""
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from abc import ABCMeta, abstractmethod
|
||||||
|
import six
|
||||||
|
|
||||||
|
from .key_mappings import PYMUX_TO_PROMPT_TOOLKIT_KEYS, pymux_key_to_prompt_toolkit_key_sequence
|
||||||
|
from .utils import get_default_shell
|
||||||
|
from .layout import Justify
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'Option',
|
||||||
|
'SetOptionError',
|
||||||
|
'OnOffOption',
|
||||||
|
'ALL_OPTIONS',
|
||||||
|
'ALL_WINDOW_OPTIONS',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Option(six.with_metaclass(ABCMeta, object)):
|
||||||
|
"""
|
||||||
|
Base class for all options.
|
||||||
|
"""
|
||||||
|
@abstractmethod
|
||||||
|
def get_all_values(self):
|
||||||
|
"""
|
||||||
|
Return a list of strings, with all possible values. (For
|
||||||
|
autocompletion.)
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def set_value(self, pymux, value):
|
||||||
|
" Set option. This can raise SetOptionError. "
|
||||||
|
|
||||||
|
|
||||||
|
class SetOptionError(Exception):
|
||||||
|
"""
|
||||||
|
Raised when setting an option fails.
|
||||||
|
"""
|
||||||
|
def __init__(self, message):
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
|
||||||
|
class OnOffOption(Option):
|
||||||
|
"""
|
||||||
|
Boolean on/off option.
|
||||||
|
"""
|
||||||
|
def __init__(self, attribute_name, window_option=False):
|
||||||
|
self.attribute_name = attribute_name
|
||||||
|
self.window_option = window_option
|
||||||
|
|
||||||
|
def get_all_values(self, pymux):
|
||||||
|
return ['on', 'off']
|
||||||
|
|
||||||
|
def set_value(self, pymux, value):
|
||||||
|
value = value.lower()
|
||||||
|
|
||||||
|
if value in ('on', 'off'):
|
||||||
|
if self.window_option:
|
||||||
|
w = pymux.arrangement.get_active_window()
|
||||||
|
setattr(w, self.attribute_name, (value == 'on'))
|
||||||
|
else:
|
||||||
|
setattr(pymux, self.attribute_name, (value == 'on'))
|
||||||
|
else:
|
||||||
|
raise SetOptionError('Expecting "yes" or "no".')
|
||||||
|
|
||||||
|
|
||||||
|
class StringOption(Option):
|
||||||
|
"""
|
||||||
|
String option, the attribute is set as a Pymux attribute.
|
||||||
|
"""
|
||||||
|
def __init__(self, attribute_name, possible_values=None):
|
||||||
|
self.attribute_name = attribute_name
|
||||||
|
self.possible_values = possible_values or []
|
||||||
|
|
||||||
|
def get_all_values(self, pymux):
|
||||||
|
return sorted(set(
|
||||||
|
self.possible_values + [getattr(pymux, self.attribute_name)]
|
||||||
|
))
|
||||||
|
|
||||||
|
def set_value(self, pymux, value):
|
||||||
|
setattr(pymux, self.attribute_name, value)
|
||||||
|
|
||||||
|
|
||||||
|
class PositiveIntOption(Option):
|
||||||
|
"""
|
||||||
|
Positive integer option, the attribute is set as a Pymux attribute.
|
||||||
|
"""
|
||||||
|
def __init__(self, attribute_name, possible_values=None):
|
||||||
|
self.attribute_name = attribute_name
|
||||||
|
self.possible_values = ['%s' % i for i in (possible_values or [])]
|
||||||
|
|
||||||
|
def get_all_values(self, pymux):
|
||||||
|
return sorted(set(
|
||||||
|
self.possible_values +
|
||||||
|
['%s' % getattr(pymux, self.attribute_name)]
|
||||||
|
))
|
||||||
|
|
||||||
|
def set_value(self, pymux, value):
|
||||||
|
"""
|
||||||
|
Take a string, and return an integer. Raise SetOptionError when the
|
||||||
|
given text does not parse to a positive integer.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
value = int(value)
|
||||||
|
if value < 0:
|
||||||
|
raise ValueError
|
||||||
|
except ValueError:
|
||||||
|
raise SetOptionError('Expecting an integer.')
|
||||||
|
else:
|
||||||
|
setattr(pymux, self.attribute_name, value)
|
||||||
|
|
||||||
|
|
||||||
|
class KeyPrefixOption(Option):
|
||||||
|
def get_all_values(self, pymux):
|
||||||
|
return PYMUX_TO_PROMPT_TOOLKIT_KEYS.keys()
|
||||||
|
|
||||||
|
def set_value(self, pymux, value):
|
||||||
|
# Translate prefix to prompt_toolkit
|
||||||
|
try:
|
||||||
|
keys = pymux_key_to_prompt_toolkit_key_sequence(value)
|
||||||
|
except ValueError:
|
||||||
|
raise SetOptionError('Invalid key: %r' % (value, ))
|
||||||
|
else:
|
||||||
|
pymux.key_bindings_manager.prefix = keys
|
||||||
|
|
||||||
|
|
||||||
|
class BaseIndexOption(Option):
|
||||||
|
" Base index for window numbering. "
|
||||||
|
def get_all_values(self, pymux):
|
||||||
|
return ['0', '1']
|
||||||
|
|
||||||
|
def set_value(self, pymux, value):
|
||||||
|
try:
|
||||||
|
value = int(value)
|
||||||
|
except ValueError:
|
||||||
|
raise SetOptionError('Expecting an integer.')
|
||||||
|
else:
|
||||||
|
pymux.arrangement.base_index = value
|
||||||
|
|
||||||
|
|
||||||
|
class KeysOption(Option):
|
||||||
|
" Emacs or Vi mode. "
|
||||||
|
def __init__(self, attribute_name):
|
||||||
|
self.attribute_name = attribute_name
|
||||||
|
|
||||||
|
def get_all_values(self, pymux):
|
||||||
|
return ['emacs', 'vi']
|
||||||
|
|
||||||
|
def set_value(self, pymux, value):
|
||||||
|
if value in ('emacs', 'vi'):
|
||||||
|
setattr(pymux, self.attribute_name, value == 'vi')
|
||||||
|
else:
|
||||||
|
raise SetOptionError('Expecting "vi" or "emacs".')
|
||||||
|
|
||||||
|
class JustifyOption(Option):
|
||||||
|
def __init__(self, attribute_name):
|
||||||
|
self.attribute_name = attribute_name
|
||||||
|
|
||||||
|
def get_all_values(self, pymux):
|
||||||
|
return Justify._ALL
|
||||||
|
|
||||||
|
def set_value(self, pymux, value):
|
||||||
|
if value in Justify._ALL:
|
||||||
|
setattr(pymux, self.attribute_name, value)
|
||||||
|
else:
|
||||||
|
raise SetOptionError('Invalid justify option.')
|
||||||
|
|
||||||
|
|
||||||
|
ALL_OPTIONS = {
|
||||||
|
'base-index': BaseIndexOption(),
|
||||||
|
'bell': OnOffOption('enable_bell'),
|
||||||
|
'history-limit': PositiveIntOption(
|
||||||
|
'history_limit', [200, 500, 1000, 2000, 5000, 10000]),
|
||||||
|
'mouse': OnOffOption('enable_mouse_support'),
|
||||||
|
'prefix': KeyPrefixOption(),
|
||||||
|
'remain-on-exit': OnOffOption('remain_on_exit'),
|
||||||
|
'status': OnOffOption('enable_status'),
|
||||||
|
'pane-border-status': OnOffOption('enable_pane_status'),
|
||||||
|
'status-keys': KeysOption('status_keys_vi_mode'),
|
||||||
|
'mode-keys': KeysOption('mode_keys_vi_mode'),
|
||||||
|
'default-terminal': StringOption(
|
||||||
|
'default_terminal', ['xterm', 'xterm-256color', 'screen']),
|
||||||
|
'status-right': StringOption('status_right'),
|
||||||
|
'status-left': StringOption('status_left'),
|
||||||
|
'status-right-length': PositiveIntOption('status_right_length', [20]),
|
||||||
|
'status-left-length': PositiveIntOption('status_left_length', [20]),
|
||||||
|
'window-status-format': StringOption('window_status_format'),
|
||||||
|
'window-status-current-format': StringOption('window_status_current_format'),
|
||||||
|
'default-shell': StringOption(
|
||||||
|
'default_shell', [get_default_shell()]),
|
||||||
|
'status-justify': JustifyOption('status_justify'),
|
||||||
|
'status-interval': PositiveIntOption(
|
||||||
|
'status_interval', [1, 2, 4, 8, 16, 30, 60]),
|
||||||
|
|
||||||
|
# Prompt-toolkit/pymux specific.
|
||||||
|
'swap-light-and-dark-colors': OnOffOption('swap_dark_and_light'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ALL_WINDOW_OPTIONS = {
|
||||||
|
'synchronize-panes': OnOffOption('synchronize_panes', window_option=True),
|
||||||
|
}
|
30
pymux/pipes/__init__.py
Normal file
30
pymux/pipes/__init__.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
"""
|
||||||
|
Platform specific (Windows+posix) implementations for inter process
|
||||||
|
communication through pipes between the Pymux server and clients.
|
||||||
|
"""
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from prompt_toolkit.utils import is_windows
|
||||||
|
from .base import PipeConnection, BrokenPipeError
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'bind_and_listen_on_socket',
|
||||||
|
|
||||||
|
# Base.
|
||||||
|
'PipeConnection',
|
||||||
|
'BrokenPipeError',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def bind_and_listen_on_socket(socket_name, accept_callback):
|
||||||
|
"""
|
||||||
|
Return socket name.
|
||||||
|
|
||||||
|
:param accept_callback: Callback is called with a `PipeConnection` as
|
||||||
|
argument.
|
||||||
|
"""
|
||||||
|
if is_windows():
|
||||||
|
from .win32_server import bind_and_listen_on_win32_socket
|
||||||
|
return bind_and_listen_on_win32_socket(socket_name, accept_callback)
|
||||||
|
else:
|
||||||
|
from .posix import bind_and_listen_on_posix_socket
|
||||||
|
return bind_and_listen_on_posix_socket(socket_name, accept_callback)
|
43
pymux/pipes/base.py
Normal file
43
pymux/pipes/base.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
from abc import ABCMeta, abstractmethod
|
||||||
|
from six import with_metaclass
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'PipeConnection',
|
||||||
|
'BrokenPipeError',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PipeConnection(with_metaclass(ABCMeta, object)):
|
||||||
|
"""
|
||||||
|
A single active Win32 pipe connection on the server side.
|
||||||
|
|
||||||
|
- Win32PipeConnection
|
||||||
|
"""
|
||||||
|
@abstractmethod
|
||||||
|
def read(self):
|
||||||
|
"""
|
||||||
|
(coroutine)
|
||||||
|
Read a single message from the pipe. (Return as text.)
|
||||||
|
|
||||||
|
This can can BrokenPipeError.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def write(self, message):
|
||||||
|
"""
|
||||||
|
(coroutine)
|
||||||
|
Write a single message into the pipe.
|
||||||
|
|
||||||
|
This can can BrokenPipeError.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def close(self):
|
||||||
|
"""
|
||||||
|
Close connection.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class BrokenPipeError(Exception):
|
||||||
|
" Raised when trying to write to or read from a broken pipe. "
|
165
pymux/pipes/posix.py
Normal file
165
pymux/pipes/posix.py
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
import getpass
|
||||||
|
import os
|
||||||
|
import six
|
||||||
|
import socket
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from prompt_toolkit.eventloop import From, Return, Future, get_event_loop
|
||||||
|
|
||||||
|
from ..log import logger
|
||||||
|
from .base import PipeConnection, BrokenPipeError
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'bind_and_listen_on_posix_socket',
|
||||||
|
'PosixSocketConnection',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def bind_and_listen_on_posix_socket(socket_name, accept_callback):
|
||||||
|
"""
|
||||||
|
:param accept_callback: Called with `PosixSocketConnection` when a new
|
||||||
|
connection is established.
|
||||||
|
"""
|
||||||
|
assert socket_name is None or isinstance(socket_name, six.text_type)
|
||||||
|
assert callable(accept_callback)
|
||||||
|
|
||||||
|
# Py2 uses 0027 and Py3 uses 0o027, but both know
|
||||||
|
# how to create the right value from the string '0027'.
|
||||||
|
old_umask = os.umask(int('0027', 8))
|
||||||
|
|
||||||
|
# Bind socket.
|
||||||
|
socket_name, socket = _bind_posix_socket(socket_name)
|
||||||
|
|
||||||
|
_ = os.umask(old_umask)
|
||||||
|
|
||||||
|
# Listen on socket.
|
||||||
|
socket.listen(0)
|
||||||
|
|
||||||
|
def _accept_cb():
|
||||||
|
connection, client_address = socket.accept()
|
||||||
|
# Note: We don't have to put this socket in non blocking mode.
|
||||||
|
# This can cause crashes when sending big packets on OS X.
|
||||||
|
|
||||||
|
posix_connection = PosixSocketConnection(connection)
|
||||||
|
|
||||||
|
accept_callback(posix_connection)
|
||||||
|
|
||||||
|
get_event_loop().add_reader(socket.fileno(), _accept_cb)
|
||||||
|
|
||||||
|
logger.info('Listening on %r.' % socket_name)
|
||||||
|
return socket_name
|
||||||
|
|
||||||
|
|
||||||
|
def _bind_posix_socket(socket_name=None):
|
||||||
|
"""
|
||||||
|
Find a socket to listen on and return it.
|
||||||
|
|
||||||
|
Returns (socket_name, sock_obj)
|
||||||
|
"""
|
||||||
|
assert socket_name is None or isinstance(socket_name, six.text_type)
|
||||||
|
|
||||||
|
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
|
||||||
|
if socket_name:
|
||||||
|
s.bind(socket_name)
|
||||||
|
return socket_name, s
|
||||||
|
else:
|
||||||
|
i = 0
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
socket_name = '%s/pymux.sock.%s.%i' % (
|
||||||
|
tempfile.gettempdir(), getpass.getuser(), i)
|
||||||
|
s.bind(socket_name)
|
||||||
|
return socket_name, s
|
||||||
|
except (OSError, socket.error):
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# When 100 times failed, cancel server
|
||||||
|
if i == 100:
|
||||||
|
logger.warning('100 times failed to listen on posix socket. '
|
||||||
|
'Please clean up old sockets.')
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
class PosixSocketConnection(PipeConnection):
|
||||||
|
"""
|
||||||
|
A single active posix pipe connection on the server side.
|
||||||
|
"""
|
||||||
|
def __init__(self, socket):
|
||||||
|
self.socket = socket
|
||||||
|
self._fd = socket.fileno()
|
||||||
|
self._recv_buffer = b''
|
||||||
|
|
||||||
|
def read(self):
|
||||||
|
r"""
|
||||||
|
Coroutine that reads the next packet.
|
||||||
|
(Packets are \0 separated.)
|
||||||
|
"""
|
||||||
|
# Read until we have a \0 in our buffer.
|
||||||
|
while b'\0' not in self._recv_buffer:
|
||||||
|
self._recv_buffer += yield From(_read_chunk_from_socket(self.socket))
|
||||||
|
|
||||||
|
# Split on the first separator.
|
||||||
|
pos = self._recv_buffer.index(b'\0')
|
||||||
|
|
||||||
|
packet = self._recv_buffer[:pos]
|
||||||
|
self._recv_buffer = self._recv_buffer[pos + 1:]
|
||||||
|
|
||||||
|
raise Return(packet)
|
||||||
|
|
||||||
|
|
||||||
|
def write(self, message):
|
||||||
|
"""
|
||||||
|
Coroutine that writes the next packet.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.socket.send(message.encode('utf-8') + b'\0')
|
||||||
|
except socket.error:
|
||||||
|
if not self._closed:
|
||||||
|
raise BrokenPipeError
|
||||||
|
|
||||||
|
return Future.succeed(None)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""
|
||||||
|
Close connection.
|
||||||
|
"""
|
||||||
|
self.socket.close()
|
||||||
|
|
||||||
|
# Make sure to remove the reader from the event loop.
|
||||||
|
get_event_loop().remove_reader(self._fd)
|
||||||
|
|
||||||
|
|
||||||
|
def _read_chunk_from_socket(socket):
|
||||||
|
"""
|
||||||
|
(coroutine)
|
||||||
|
Turn socket reading into coroutine.
|
||||||
|
"""
|
||||||
|
fd = socket.fileno()
|
||||||
|
f = Future()
|
||||||
|
|
||||||
|
def read_callback():
|
||||||
|
get_event_loop().remove_reader(fd)
|
||||||
|
|
||||||
|
# Read next chunk.
|
||||||
|
try:
|
||||||
|
data = socket.recv(1024)
|
||||||
|
except OSError as e:
|
||||||
|
# On OSX, when we try to create a new window by typing "pymux
|
||||||
|
# new-window" in a centain pane, very often we get the following
|
||||||
|
# error: "OSError: [Errno 9] Bad file descriptor."
|
||||||
|
# This doesn't seem very harmful, and we can just try again.
|
||||||
|
logger.warning('Got OSError while reading data from client: %s. '
|
||||||
|
'Trying again.', e)
|
||||||
|
f.set_result('')
|
||||||
|
return
|
||||||
|
|
||||||
|
if data:
|
||||||
|
f.set_result(data)
|
||||||
|
else:
|
||||||
|
f.set_exception(BrokenPipeError)
|
||||||
|
|
||||||
|
get_event_loop().add_reader(fd, read_callback)
|
||||||
|
|
||||||
|
return f
|
205
pymux/pipes/win32.py
Normal file
205
pymux/pipes/win32.py
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
"""
|
||||||
|
Common Win32 pipe operations.
|
||||||
|
"""
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from ctypes import windll, byref, create_string_buffer
|
||||||
|
from ctypes.wintypes import DWORD, BOOL
|
||||||
|
from prompt_toolkit.eventloop import get_event_loop, From, Return, Future
|
||||||
|
from ptterm.backends.win32_pipes import OVERLAPPED
|
||||||
|
from .base import BrokenPipeError
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'read_message_from_pipe',
|
||||||
|
'read_message_bytes_from_pipe',
|
||||||
|
'write_message_to_pipe',
|
||||||
|
'write_message_bytes_to_pipe',
|
||||||
|
'wait_for_event',
|
||||||
|
]
|
||||||
|
|
||||||
|
BUFSIZE = 4096
|
||||||
|
|
||||||
|
GENERIC_READ = 0x80000000
|
||||||
|
GENERIC_WRITE = 0x40000000
|
||||||
|
OPEN_EXISTING = 0x3
|
||||||
|
|
||||||
|
ERROR_BROKEN_PIPE = 109
|
||||||
|
ERROR_IO_PENDING = 997
|
||||||
|
ERROR_MORE_DATA = 234
|
||||||
|
ERROR_NO_DATA = 232
|
||||||
|
FILE_FLAG_OVERLAPPED = 0x40000000
|
||||||
|
|
||||||
|
PIPE_READMODE_MESSAGE = 0x2
|
||||||
|
FILE_WRITE_ATTRIBUTES = 0x100 # 256
|
||||||
|
INVALID_HANDLE_VALUE = -1
|
||||||
|
|
||||||
|
|
||||||
|
def connect_to_pipe(pipe_name):
|
||||||
|
"""
|
||||||
|
Connect to a new pipe in message mode.
|
||||||
|
"""
|
||||||
|
pipe_handle = windll.kernel32.CreateFileW(
|
||||||
|
pipe_name,
|
||||||
|
DWORD(GENERIC_READ | GENERIC_WRITE | FILE_WRITE_ATTRIBUTES),
|
||||||
|
DWORD(0), # No sharing.
|
||||||
|
None, # Default security attributes.
|
||||||
|
DWORD(OPEN_EXISTING), # dwCreationDisposition.
|
||||||
|
FILE_FLAG_OVERLAPPED, # dwFlagsAndAttributes.
|
||||||
|
None # hTemplateFile,
|
||||||
|
)
|
||||||
|
if pipe_handle == INVALID_HANDLE_VALUE:
|
||||||
|
raise Exception('Invalid handle. Connecting to pipe %r failed.' % pipe_name)
|
||||||
|
|
||||||
|
# Turn pipe into message mode.
|
||||||
|
dwMode = DWORD(PIPE_READMODE_MESSAGE)
|
||||||
|
windll.kernel32.SetNamedPipeHandleState(
|
||||||
|
pipe_handle,
|
||||||
|
byref(dwMode),
|
||||||
|
None,
|
||||||
|
None)
|
||||||
|
|
||||||
|
return pipe_handle
|
||||||
|
|
||||||
|
|
||||||
|
def create_event():
|
||||||
|
"""
|
||||||
|
Create Win32 event.
|
||||||
|
"""
|
||||||
|
event = windll.kernel32.CreateEventA(
|
||||||
|
None, # Default security attributes.
|
||||||
|
BOOL(True), # Manual reset event.
|
||||||
|
BOOL(True), # Initial state = signaled.
|
||||||
|
None # Unnamed event object.
|
||||||
|
)
|
||||||
|
if not event:
|
||||||
|
raise Exception('event creation failed.')
|
||||||
|
return event
|
||||||
|
|
||||||
|
|
||||||
|
def read_message_from_pipe(pipe_handle):
|
||||||
|
"""
|
||||||
|
(coroutine)
|
||||||
|
Read message from this pipe. Return text.
|
||||||
|
"""
|
||||||
|
data = yield From(read_message_bytes_from_pipe(pipe_handle))
|
||||||
|
assert isinstance(data, bytes)
|
||||||
|
raise Return(data.decode('utf-8', 'ignore'))
|
||||||
|
|
||||||
|
|
||||||
|
def read_message_bytes_from_pipe(pipe_handle):
|
||||||
|
"""
|
||||||
|
(coroutine)
|
||||||
|
Read message from this pipe. Return bytes.
|
||||||
|
"""
|
||||||
|
overlapped = OVERLAPPED()
|
||||||
|
overlapped.hEvent = create_event()
|
||||||
|
|
||||||
|
try:
|
||||||
|
buff = create_string_buffer(BUFSIZE + 1)
|
||||||
|
c_read = DWORD()
|
||||||
|
|
||||||
|
success = windll.kernel32.ReadFile(
|
||||||
|
pipe_handle,
|
||||||
|
buff,
|
||||||
|
DWORD(BUFSIZE),
|
||||||
|
byref(c_read),
|
||||||
|
byref(overlapped))
|
||||||
|
|
||||||
|
if success:
|
||||||
|
buff[c_read.value] = b'\0'
|
||||||
|
raise Return(buff.value)
|
||||||
|
|
||||||
|
error_code = windll.kernel32.GetLastError()
|
||||||
|
|
||||||
|
if error_code == ERROR_IO_PENDING:
|
||||||
|
yield From(wait_for_event(overlapped.hEvent))
|
||||||
|
|
||||||
|
success = windll.kernel32.GetOverlappedResult(
|
||||||
|
pipe_handle,
|
||||||
|
byref(overlapped),
|
||||||
|
byref(c_read),
|
||||||
|
BOOL(False))
|
||||||
|
|
||||||
|
if success:
|
||||||
|
buff[c_read.value] = b'\0'
|
||||||
|
raise Return(buff.value)
|
||||||
|
|
||||||
|
else:
|
||||||
|
error_code = windll.kernel32.GetLastError()
|
||||||
|
if error_code == ERROR_BROKEN_PIPE:
|
||||||
|
raise BrokenPipeError
|
||||||
|
|
||||||
|
elif error_code == ERROR_MORE_DATA:
|
||||||
|
more_data = yield From(read_message_bytes_from_pipe(pipe_handle))
|
||||||
|
raise Return(buff.value + more_data)
|
||||||
|
else:
|
||||||
|
raise Exception(
|
||||||
|
'reading overlapped IO failed. error_code=%r' % error_code)
|
||||||
|
|
||||||
|
elif error_code == ERROR_BROKEN_PIPE:
|
||||||
|
raise BrokenPipeError
|
||||||
|
|
||||||
|
elif error_code == ERROR_MORE_DATA:
|
||||||
|
more_data = yield From(read_message_bytes_from_pipe(pipe_handle))
|
||||||
|
raise Return(buff.value + more_data)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise Exception('Reading pipe failed, error_code=%s' % error_code)
|
||||||
|
finally:
|
||||||
|
windll.kernel32.CloseHandle(overlapped.hEvent)
|
||||||
|
|
||||||
|
|
||||||
|
def write_message_to_pipe(pipe_handle, text):
|
||||||
|
data = text.encode('utf-8')
|
||||||
|
yield From(write_message_bytes_to_pipe(pipe_handle, data))
|
||||||
|
|
||||||
|
|
||||||
|
def write_message_bytes_to_pipe(pipe_handle, data):
|
||||||
|
overlapped = OVERLAPPED()
|
||||||
|
overlapped.hEvent = create_event()
|
||||||
|
|
||||||
|
try:
|
||||||
|
c_written = DWORD()
|
||||||
|
|
||||||
|
success = windll.kernel32.WriteFile(
|
||||||
|
pipe_handle,
|
||||||
|
create_string_buffer(data),
|
||||||
|
len(data),
|
||||||
|
byref(c_written),
|
||||||
|
byref(overlapped))
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return
|
||||||
|
|
||||||
|
error_code = windll.kernel32.GetLastError()
|
||||||
|
if error_code == ERROR_IO_PENDING:
|
||||||
|
yield From(wait_for_event(overlapped.hEvent))
|
||||||
|
|
||||||
|
success = windll.kernel32.GetOverlappedResult(
|
||||||
|
pipe_handle,
|
||||||
|
byref(overlapped),
|
||||||
|
byref(c_written),
|
||||||
|
BOOL(False))
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
error_code = windll.kernel32.GetLastError()
|
||||||
|
if error_code == ERROR_BROKEN_PIPE:
|
||||||
|
raise BrokenPipeError
|
||||||
|
else:
|
||||||
|
raise Exception('Writing overlapped IO failed. error_code=%r' % error_code)
|
||||||
|
|
||||||
|
elif error_code == ERROR_BROKEN_PIPE:
|
||||||
|
raise BrokenPipeError
|
||||||
|
finally:
|
||||||
|
windll.kernel32.CloseHandle(overlapped.hEvent)
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_event(event):
|
||||||
|
"""
|
||||||
|
Wraps a win32 event into a `Future` and wait for it.
|
||||||
|
"""
|
||||||
|
f = Future()
|
||||||
|
def ready():
|
||||||
|
get_event_loop().remove_win32_handle(event)
|
||||||
|
f.set_result(None)
|
||||||
|
get_event_loop().add_win32_handle(event, ready)
|
||||||
|
return f
|
41
pymux/pipes/win32_client.py
Normal file
41
pymux/pipes/win32_client.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
from .win32 import read_message_from_pipe, write_message_to_pipe, connect_to_pipe
|
||||||
|
from ctypes import windll
|
||||||
|
from prompt_toolkit.eventloop import From, Return
|
||||||
|
import six
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'PipeClient',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PipeClient(object):
|
||||||
|
r"""
|
||||||
|
Windows pipe client.
|
||||||
|
|
||||||
|
:param pipe_name: Name of the pipe. E.g. \\.\pipe\pipe_name
|
||||||
|
"""
|
||||||
|
def __init__(self, pipe_name):
|
||||||
|
assert isinstance(pipe_name, six.text_type)
|
||||||
|
self.pipe_handle = connect_to_pipe(pipe_name)
|
||||||
|
|
||||||
|
def write_message(self, text):
|
||||||
|
"""
|
||||||
|
(coroutine)
|
||||||
|
Write message into the pipe.
|
||||||
|
"""
|
||||||
|
yield From(write_message_to_pipe(self.pipe_handle, text))
|
||||||
|
|
||||||
|
def read_message(self):
|
||||||
|
"""
|
||||||
|
(coroutine)
|
||||||
|
Read one single message from the pipe and return as text.
|
||||||
|
"""
|
||||||
|
message = yield From(read_message_from_pipe(self.pipe_handle))
|
||||||
|
raise Return(message)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""
|
||||||
|
Close the connection.
|
||||||
|
"""
|
||||||
|
windll.kernel32.CloseHandle(self.pipe_handle)
|
170
pymux/pipes/win32_server.py
Normal file
170
pymux/pipes/win32_server.py
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
from ctypes import windll, byref
|
||||||
|
from ctypes.wintypes import DWORD
|
||||||
|
from prompt_toolkit.eventloop import From, Future, Return, ensure_future
|
||||||
|
from ptterm.backends.win32_pipes import OVERLAPPED
|
||||||
|
|
||||||
|
from .win32 import wait_for_event, create_event, read_message_from_pipe, write_message_to_pipe
|
||||||
|
from .base import PipeConnection, BrokenPipeError
|
||||||
|
from ..log import logger
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'bind_and_listen_on_win32_socket',
|
||||||
|
'Win32PipeConnection',
|
||||||
|
'PipeInstance',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
INSTANCES = 10
|
||||||
|
BUFSIZE = 4096
|
||||||
|
|
||||||
|
# CreateNamedPipeW flags.
|
||||||
|
# See: https://docs.microsoft.com/en-us/windows/desktop/api/winbase/nf-winbase-createnamedpipea
|
||||||
|
PIPE_ACCESS_DUPLEX = 0x00000003
|
||||||
|
FILE_FLAG_OVERLAPPED = 0x40000000
|
||||||
|
PIPE_TYPE_MESSAGE = 0x00000004
|
||||||
|
PIPE_READMODE_MESSAGE = 0x00000002
|
||||||
|
PIPE_WAIT = 0x00000000
|
||||||
|
PIPE_NOWAIT = 0x00000001
|
||||||
|
|
||||||
|
ERROR_IO_PENDING = 997
|
||||||
|
ERROR_BROKEN_PIPE= 109
|
||||||
|
ERROR_NO_DATA = 232
|
||||||
|
|
||||||
|
CONNECTING_STATE = 0
|
||||||
|
READING_STATE = 1
|
||||||
|
WRITING_STATE = 2
|
||||||
|
|
||||||
|
|
||||||
|
def bind_and_listen_on_win32_socket(socket_name, accept_callback):
|
||||||
|
"""
|
||||||
|
:param accept_callback: Called with `Win32PipeConnection` when a new
|
||||||
|
connection is established.
|
||||||
|
"""
|
||||||
|
assert callable(accept_callback)
|
||||||
|
socket_name = r'\\.\pipe\pymux.sock.jonathan.42'
|
||||||
|
|
||||||
|
pipes = [PipeInstance(socket_name, pipe_connection_cb=accept_callback)
|
||||||
|
for i in range(INSTANCES)]
|
||||||
|
|
||||||
|
for p in pipes:
|
||||||
|
# Start pipe.
|
||||||
|
ensure_future(p.handle_pipe())
|
||||||
|
|
||||||
|
return socket_name
|
||||||
|
|
||||||
|
|
||||||
|
class Win32PipeConnection(PipeConnection):
|
||||||
|
"""
|
||||||
|
A single active Win32 pipe connection on the server side.
|
||||||
|
"""
|
||||||
|
def __init__(self, pipe_instance):
|
||||||
|
assert isinstance(pipe_instance, PipeInstance)
|
||||||
|
self.pipe_instance = pipe_instance
|
||||||
|
self.done_f = Future()
|
||||||
|
|
||||||
|
def read(self):
|
||||||
|
"""
|
||||||
|
(coroutine)
|
||||||
|
Read a single message from the pipe. (Return as text.)
|
||||||
|
"""
|
||||||
|
if self.done_f.done():
|
||||||
|
raise BrokenPipeError
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = yield From(read_message_from_pipe(self.pipe_instance.pipe_handle))
|
||||||
|
raise Return(result)
|
||||||
|
except BrokenPipeError:
|
||||||
|
self.done_f.set_result(None)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def write(self, message):
|
||||||
|
"""
|
||||||
|
(coroutine)
|
||||||
|
Write a single message into the pipe.
|
||||||
|
"""
|
||||||
|
if self.done_f.done():
|
||||||
|
raise BrokenPipeError
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield From(write_message_to_pipe(self.pipe_instance.pipe_handle, message))
|
||||||
|
except BrokenPipeError:
|
||||||
|
self.done_f.set_result(None)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PipeInstance(object):
|
||||||
|
def __init__(self, pipe_name, instances=INSTANCES, buffsize=BUFSIZE,
|
||||||
|
timeout=5000, pipe_connection_cb=None):
|
||||||
|
|
||||||
|
self.pipe_handle = windll.kernel32.CreateNamedPipeW(
|
||||||
|
pipe_name, # Pipe name.
|
||||||
|
PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
|
||||||
|
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
|
||||||
|
DWORD(instances), # Max instances. (TODO: increase).
|
||||||
|
DWORD(buffsize), # Output buffer size.
|
||||||
|
DWORD(buffsize), # Input buffer size.
|
||||||
|
DWORD(timeout), # Client time-out.
|
||||||
|
None, # Default security attributes.
|
||||||
|
)
|
||||||
|
self.pipe_connection_cb = pipe_connection_cb
|
||||||
|
|
||||||
|
if not self.pipe_handle:
|
||||||
|
raise Exception('invalid pipe')
|
||||||
|
|
||||||
|
def handle_pipe(self):
|
||||||
|
"""
|
||||||
|
Coroutine that handles this pipe.
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
yield From(self._handle_client())
|
||||||
|
|
||||||
|
def _handle_client(self):
|
||||||
|
"""
|
||||||
|
Coroutine that connects to a single client and handles that.
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
# Wait for connection.
|
||||||
|
logger.info('Waiting for connection in pipe instance.')
|
||||||
|
yield From(self._connect_client())
|
||||||
|
logger.info('Connected in pipe instance')
|
||||||
|
|
||||||
|
conn = Win32PipeConnection(self)
|
||||||
|
self.pipe_connection_cb(conn)
|
||||||
|
|
||||||
|
yield From(conn.done_f)
|
||||||
|
logger.info('Pipe instance done.')
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Disconnect and reconnect.
|
||||||
|
logger.info('Disconnecting pipe instance.')
|
||||||
|
windll.kernel32.DisconnectNamedPipe(self.pipe_handle)
|
||||||
|
|
||||||
|
def _connect_client(self):
|
||||||
|
"""
|
||||||
|
Wait for a client to connect to this pipe.
|
||||||
|
"""
|
||||||
|
overlapped = OVERLAPPED()
|
||||||
|
overlapped.hEvent = create_event()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
success = windll.kernel32.ConnectNamedPipe(
|
||||||
|
self.pipe_handle,
|
||||||
|
byref(overlapped))
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return
|
||||||
|
|
||||||
|
last_error = windll.kernel32.GetLastError()
|
||||||
|
if last_error == ERROR_IO_PENDING:
|
||||||
|
yield From(wait_for_event(overlapped.hEvent))
|
||||||
|
|
||||||
|
# XXX: Call GetOverlappedResult.
|
||||||
|
return # Connection succeeded.
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise Exception('connect failed with error code' + str(last_error))
|
83
pymux/rc.py
Normal file
83
pymux/rc.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
"""
|
||||||
|
Initial configuration.
|
||||||
|
"""
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'STARTUP_COMMANDS'
|
||||||
|
)
|
||||||
|
|
||||||
|
STARTUP_COMMANDS = """
|
||||||
|
bind-key '"' split-window -v
|
||||||
|
bind-key % split-window -h
|
||||||
|
bind-key c new-window
|
||||||
|
bind-key Right select-pane -R
|
||||||
|
bind-key Left select-pane -L
|
||||||
|
bind-key Up select-pane -U
|
||||||
|
bind-key Down select-pane -D
|
||||||
|
bind-key C-l select-pane -R
|
||||||
|
bind-key C-h select-pane -L
|
||||||
|
bind-key C-j select-pane -D
|
||||||
|
bind-key C-k select-pane -U
|
||||||
|
bind-key ; last-pane
|
||||||
|
bind-key ! break-pane
|
||||||
|
bind-key d detach-client
|
||||||
|
bind-key t clock-mode
|
||||||
|
bind-key Space next-layout
|
||||||
|
bind-key C-z suspend-client
|
||||||
|
|
||||||
|
bind-key z resize-pane -Z
|
||||||
|
bind-key k resize-pane -U 2
|
||||||
|
bind-key j resize-pane -D 2
|
||||||
|
bind-key h resize-pane -L 2
|
||||||
|
bind-key l resize-pane -R 2
|
||||||
|
bind-key q display-panes
|
||||||
|
bind-key C-Up resize-pane -U 2
|
||||||
|
bind-key C-Down resize-pane -D 2
|
||||||
|
bind-key C-Left resize-pane -L 2
|
||||||
|
bind-key C-Right resize-pane -R 2
|
||||||
|
bind-key M-Up resize-pane -U 5
|
||||||
|
bind-key M-Down resize-pane -D 5
|
||||||
|
bind-key M-Left resize-pane -L 5
|
||||||
|
bind-key M-Right resize-pane -R 5
|
||||||
|
|
||||||
|
bind-key : command-prompt
|
||||||
|
bind-key 0 select-window -t :0
|
||||||
|
bind-key 1 select-window -t :1
|
||||||
|
bind-key 2 select-window -t :2
|
||||||
|
bind-key 3 select-window -t :3
|
||||||
|
bind-key 4 select-window -t :4
|
||||||
|
bind-key 5 select-window -t :5
|
||||||
|
bind-key 6 select-window -t :6
|
||||||
|
bind-key 7 select-window -t :7
|
||||||
|
bind-key 8 select-window -t :8
|
||||||
|
bind-key 9 select-window -t :9
|
||||||
|
bind-key n next-window
|
||||||
|
bind-key p previous-window
|
||||||
|
bind-key o select-pane -t :.+
|
||||||
|
bind-key { swap-pane -U
|
||||||
|
bind-key } swap-pane -D
|
||||||
|
bind-key x confirm-before -p "kill-pane #P?" kill-pane
|
||||||
|
bind-key & confirm-before -p "kill-window #W?" kill-window
|
||||||
|
bind-key C-o rotate-window
|
||||||
|
bind-key M-o rotate-window -D
|
||||||
|
bind-key C-b send-prefix
|
||||||
|
bind-key . command-prompt "move-window -t '%%'"
|
||||||
|
bind-key [ copy-mode
|
||||||
|
bind-key ] paste-buffer
|
||||||
|
bind-key ? list-keys
|
||||||
|
bind-key PPage copy-mode -u
|
||||||
|
|
||||||
|
# Layouts.
|
||||||
|
bind-key M-1 select-layout even-horizontal
|
||||||
|
bind-key M-2 select-layout even-vertical
|
||||||
|
bind-key M-3 select-layout main-horizontal
|
||||||
|
bind-key M-4 select-layout main-vertical
|
||||||
|
bind-key M-5 select-layout tiled
|
||||||
|
|
||||||
|
# Renaming stuff.
|
||||||
|
bind-key , command-prompt -I #W "rename-window '%%'"
|
||||||
|
#bind-key "'" command-prompt -I #W "rename-pane '%%'"
|
||||||
|
bind-key "'" command-prompt -p index "select-window -t ':%%'"
|
||||||
|
bind-key . command-prompt "move-window -t '%%'"
|
||||||
|
"""
|
233
pymux/server.py
Normal file
233
pymux/server.py
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
import json
|
||||||
|
|
||||||
|
from prompt_toolkit.application.current import set_app
|
||||||
|
from prompt_toolkit.eventloop import ensure_future, From
|
||||||
|
from prompt_toolkit.input.vt100_parser import Vt100Parser
|
||||||
|
from prompt_toolkit.layout.screen import Size
|
||||||
|
from prompt_toolkit.output.vt100 import Vt100_Output
|
||||||
|
from prompt_toolkit.utils import is_windows
|
||||||
|
|
||||||
|
from .log import logger
|
||||||
|
from .pipes import BrokenPipeError
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'ServerConnection',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ServerConnection(object):
|
||||||
|
"""
|
||||||
|
For each client that connects, we have one instance of this class.
|
||||||
|
"""
|
||||||
|
def __init__(self, pymux, pipe_connection):
|
||||||
|
self.pymux = pymux
|
||||||
|
|
||||||
|
self.pipe_connection = pipe_connection
|
||||||
|
|
||||||
|
self.size = Size(rows=20, columns=80)
|
||||||
|
self._closed = False
|
||||||
|
|
||||||
|
self._recv_buffer = b''
|
||||||
|
self.client_state = None
|
||||||
|
|
||||||
|
def feed_key(key):
|
||||||
|
self.client_state.app.key_processor.feed(key)
|
||||||
|
self.client_state.app.key_processor.process_keys()
|
||||||
|
|
||||||
|
self._inputstream = Vt100Parser(feed_key)
|
||||||
|
self._pipeinput = _ClientInput(self._send_packet)
|
||||||
|
|
||||||
|
ensure_future(self._start_reading())
|
||||||
|
|
||||||
|
def _start_reading(self):
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
data = yield From(self.pipe_connection.read())
|
||||||
|
self._process(data)
|
||||||
|
except BrokenPipeError:
|
||||||
|
self.detach_and_close()
|
||||||
|
break
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
import traceback; traceback.print_stack()
|
||||||
|
print('got exception ', repr(e))
|
||||||
|
break
|
||||||
|
|
||||||
|
def _process(self, data):
|
||||||
|
"""
|
||||||
|
Process packet received from client.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
packet = json.loads(data)
|
||||||
|
except ValueError:
|
||||||
|
# So far, this never happened. But it would be good to have some
|
||||||
|
# protection.
|
||||||
|
logger.warning('Received invalid JSON from client. Ignoring.')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Handle commands.
|
||||||
|
if packet['cmd'] == 'run-command':
|
||||||
|
self._run_command(packet)
|
||||||
|
|
||||||
|
# Handle stdin.
|
||||||
|
elif packet['cmd'] == 'in':
|
||||||
|
self._pipeinput.send_text(packet['data'])
|
||||||
|
|
||||||
|
# elif packet['cmd'] == 'flush-input':
|
||||||
|
# self._inputstream.flush() # Flush escape key. # XXX: I think we no longer need this.
|
||||||
|
|
||||||
|
# Set size. (The client reports the size.)
|
||||||
|
elif packet['cmd'] == 'size':
|
||||||
|
data = packet['data']
|
||||||
|
self.size = Size(rows=data[0], columns=data[1])
|
||||||
|
self.pymux.invalidate()
|
||||||
|
|
||||||
|
# Start GUI. (Create CommandLineInterface front-end for pymux.)
|
||||||
|
elif packet['cmd'] == 'start-gui':
|
||||||
|
detach_other_clients = bool(packet['detach-others'])
|
||||||
|
color_depth = packet['color-depth']
|
||||||
|
term = packet['term']
|
||||||
|
|
||||||
|
if detach_other_clients:
|
||||||
|
for c in self.pymux.connections:
|
||||||
|
c.detach_and_close()
|
||||||
|
|
||||||
|
print('Create app...')
|
||||||
|
self._create_app(color_depth=color_depth, term=term)
|
||||||
|
|
||||||
|
def _send_packet(self, data):
|
||||||
|
"""
|
||||||
|
Send packet to client.
|
||||||
|
"""
|
||||||
|
if self._closed:
|
||||||
|
return
|
||||||
|
|
||||||
|
data = json.dumps(data)
|
||||||
|
|
||||||
|
def send():
|
||||||
|
try:
|
||||||
|
yield From(self.pipe_connection.write(data))
|
||||||
|
except BrokenPipeError:
|
||||||
|
self.detach_and_close()
|
||||||
|
ensure_future(send())
|
||||||
|
|
||||||
|
def _run_command(self, packet):
|
||||||
|
"""
|
||||||
|
Execute a run command from the client.
|
||||||
|
"""
|
||||||
|
create_temp_cli = self.client_states is None
|
||||||
|
|
||||||
|
if create_temp_cli:
|
||||||
|
# If this client doesn't have a CLI. Create a Fake CLI where the
|
||||||
|
# window containing this pane, is the active one. (The CLI instance
|
||||||
|
# will be removed before the render function is called, so it doesn't
|
||||||
|
# hurt too much and makes the code easier.)
|
||||||
|
pane_id = int(packet['pane_id'])
|
||||||
|
self._create_app()
|
||||||
|
with set_app(self.client_state.app):
|
||||||
|
self.pymux.arrangement.set_active_window_from_pane_id(pane_id)
|
||||||
|
|
||||||
|
with set_app(self.client_state.app):
|
||||||
|
try:
|
||||||
|
self.pymux.handle_command(packet['data'])
|
||||||
|
finally:
|
||||||
|
self._close_connection()
|
||||||
|
|
||||||
|
def _create_app(self, color_depth, term='xterm'):
|
||||||
|
"""
|
||||||
|
Create CommandLineInterface for this client.
|
||||||
|
Called when the client wants to attach the UI to the server.
|
||||||
|
"""
|
||||||
|
output = Vt100_Output(_SocketStdout(self._send_packet),
|
||||||
|
lambda: self.size,
|
||||||
|
term=term,
|
||||||
|
write_binary=False)
|
||||||
|
|
||||||
|
self.client_state = self.pymux.add_client(
|
||||||
|
input=self._pipeinput, output=output, connection=self, color_depth=color_depth)
|
||||||
|
|
||||||
|
print('Start running app...')
|
||||||
|
future = self.client_state.app.run_async()
|
||||||
|
print('Start running app got future...', future)
|
||||||
|
|
||||||
|
@future.add_done_callback
|
||||||
|
def done(_):
|
||||||
|
print('APP DONE.........')
|
||||||
|
print(future.result())
|
||||||
|
self._close_connection()
|
||||||
|
|
||||||
|
def _close_connection(self):
|
||||||
|
# This is important. If we would forget this, the server will
|
||||||
|
# render CLI output for clients that aren't connected anymore.
|
||||||
|
self.pymux.remove_client(self)
|
||||||
|
self.client_state = None
|
||||||
|
self._closed = True
|
||||||
|
|
||||||
|
# Remove from eventloop.
|
||||||
|
self.pipe_connection.close()
|
||||||
|
|
||||||
|
def suspend_client_to_background(self):
|
||||||
|
"""
|
||||||
|
Ask the client to suspend itself. (Like, when Ctrl-Z is pressed.)
|
||||||
|
"""
|
||||||
|
self._send_packet({'cmd': 'suspend'})
|
||||||
|
|
||||||
|
def detach_and_close(self):
|
||||||
|
# Remove from Pymux.
|
||||||
|
self._close_connection()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class _SocketStdout(object):
|
||||||
|
"""
|
||||||
|
Stdout-like object that writes everything through the unix socket to the
|
||||||
|
client.
|
||||||
|
"""
|
||||||
|
def __init__(self, send_packet):
|
||||||
|
assert callable(send_packet)
|
||||||
|
self.send_packet = send_packet
|
||||||
|
self._buffer = []
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
self._buffer.append(data)
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
data = {'cmd': 'out', 'data': ''.join(self._buffer)}
|
||||||
|
self.send_packet(data)
|
||||||
|
self._buffer = []
|
||||||
|
|
||||||
|
|
||||||
|
if is_windows():
|
||||||
|
from prompt_toolkit.input.win32_pipe import Win32PipeInput as PipeInput
|
||||||
|
else:
|
||||||
|
from prompt_toolkit.input.posix_pipe import PosixPipeInput as PipeInput
|
||||||
|
|
||||||
|
class _ClientInput(PipeInput):
|
||||||
|
"""
|
||||||
|
Input class that can be given to the CommandLineInterface.
|
||||||
|
We only need this for turning the client into raw_mode/cooked_mode.
|
||||||
|
"""
|
||||||
|
def __init__(self, send_packet):
|
||||||
|
super(_ClientInput, self).__init__()
|
||||||
|
assert callable(send_packet)
|
||||||
|
self.send_packet = send_packet
|
||||||
|
|
||||||
|
# Implement raw/cooked mode by sending this to the attached client.
|
||||||
|
|
||||||
|
def raw_mode(self):
|
||||||
|
return self._create_context_manager('raw')
|
||||||
|
|
||||||
|
def cooked_mode(self):
|
||||||
|
return self._create_context_manager('cooked')
|
||||||
|
|
||||||
|
def _create_context_manager(self, mode):
|
||||||
|
" Create a context manager that sends 'mode' commands to the client. "
|
||||||
|
class mode_context_manager(object):
|
||||||
|
def __enter__(*a):
|
||||||
|
self.send_packet({'cmd': 'mode', 'data': mode})
|
||||||
|
|
||||||
|
def __exit__(*a):
|
||||||
|
self.send_packet({'cmd': 'mode', 'data': 'restore'})
|
||||||
|
|
||||||
|
return mode_context_manager()
|
79
pymux/style.py
Normal file
79
pymux/style.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
"""
|
||||||
|
The color scheme.
|
||||||
|
"""
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from prompt_toolkit.styles import Style, Priority
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'ui_style',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
ui_style = Style.from_dict({
|
||||||
|
'border': '#888888',
|
||||||
|
'terminal.focused border': 'ansigreen bold',
|
||||||
|
|
||||||
|
#'terminal titleba': 'bg:#aaaaaa #dddddd ',
|
||||||
|
'terminal titlebar': 'bg:#888888 #ffffff',
|
||||||
|
# 'terminal titlebar paneindex': 'bg:#888888 #000000',
|
||||||
|
|
||||||
|
'terminal.focused titlebar': 'bg:#448844 #ffffff',
|
||||||
|
'terminal.focused titlebar name': 'bg:#88aa44 #ffffff',
|
||||||
|
'terminal.focused titlebar paneindex': 'bg:#ff0000',
|
||||||
|
|
||||||
|
# 'titlebar title': '',
|
||||||
|
# 'titlebar name': '#ffffff noitalic',
|
||||||
|
# 'focused-terminal titlebar name': 'bg:#88aa44',
|
||||||
|
# 'titlebar.line': '#444444',
|
||||||
|
# 'titlebar.line focused': '#448844 noinherit',
|
||||||
|
# 'titlebar focused': 'bg:#5f875f #ffffff bold',
|
||||||
|
# 'titlebar.title focused': '',
|
||||||
|
# 'titlebar.zoom': 'bg:#884400 #ffffff',
|
||||||
|
# 'titlebar paneindex': '',
|
||||||
|
# 'titlebar.copymode': 'bg:#88aa88 #444444',
|
||||||
|
# 'titlebar.copymode.position': '',
|
||||||
|
|
||||||
|
# 'focused-terminal titlebar.copymode': 'bg:#aaff44 #000000',
|
||||||
|
# 'titlebar.copymode.position': '#888888',
|
||||||
|
|
||||||
|
'commandline': 'bg:#4e4e4e #ffffff',
|
||||||
|
'commandline.command': 'bold',
|
||||||
|
'commandline.prompt': 'bold',
|
||||||
|
#'statusbar': 'noreverse bg:#448844 #000000',
|
||||||
|
'statusbar': 'noreverse bg:ansigreen #000000',
|
||||||
|
'statusbar window': '#ffffff',
|
||||||
|
'statusbar window.current': 'bg:#44ff44 #000000',
|
||||||
|
'auto-suggestion': 'bg:#4e5e4e #88aa88',
|
||||||
|
######################################################################################################## DECODED
|
||||||
|
'decoded': 'bg:#000000 #ff0000',
|
||||||
|
######################################################################################################## DECODED
|
||||||
|
'message': 'bg:#bbee88 #222222',
|
||||||
|
'background': '#888888',
|
||||||
|
'clock': 'bg:#88aa00',
|
||||||
|
'panenumber': 'bg:#888888',
|
||||||
|
'panenumber focused': 'bg:#aa8800',
|
||||||
|
'terminated': 'bg:#aa0000 #ffffff',
|
||||||
|
|
||||||
|
'confirmationtoolbar': 'bg:#880000 #ffffff',
|
||||||
|
'confirmationtoolbar question': '',
|
||||||
|
'confirmationtoolbar yesno': 'bg:#440000',
|
||||||
|
|
||||||
|
'copy-mode-cursor-position': 'bg:ansiyellow ansiblack',
|
||||||
|
|
||||||
|
# 'search-toolbar': 'bg:#88ff44 #444444',
|
||||||
|
'search-toolbar.prompt': 'bg:#88ff44 #444444',
|
||||||
|
'search-toolbar.text': 'bg:#88ff44 #000000',
|
||||||
|
# 'search-toolbar focused': 'bg:#aaff44 #444444',
|
||||||
|
# 'search-toolbar.text focused': 'bold #000000',
|
||||||
|
|
||||||
|
'search-match': '#000000 bg:#88aa88',
|
||||||
|
'search-match.current': '#000000 bg:#aaffaa underline',
|
||||||
|
|
||||||
|
# Pop-up dialog. Ignore built-in style.
|
||||||
|
'dialog': 'noinherit',
|
||||||
|
'dialog.body': 'noinherit',
|
||||||
|
'dialog frame': 'noinherit',
|
||||||
|
'dialog.body text-area': 'noinherit',
|
||||||
|
'dialog.body text-area last-line': 'noinherit',
|
||||||
|
|
||||||
|
}, priority=Priority.MOST_PRECISE)
|
106
pymux/utils.py
Normal file
106
pymux/utils.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
"""
|
||||||
|
Some utilities.
|
||||||
|
"""
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from prompt_toolkit.utils import is_windows
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'daemonize',
|
||||||
|
'nonblocking',
|
||||||
|
'get_default_shell',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def daemonize(stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
|
||||||
|
"""
|
||||||
|
Double fork-trick. For starting a posix daemon.
|
||||||
|
|
||||||
|
This forks the current process into a daemon. The stdin, stdout, and stderr
|
||||||
|
arguments are file names that will be opened and be used to replace the
|
||||||
|
standard file descriptors in sys.stdin, sys.stdout, and sys.stderr. These
|
||||||
|
arguments are optional and default to /dev/null. Note that stderr is opened
|
||||||
|
unbuffered, so if it shares a file with stdout then interleaved output may
|
||||||
|
not appear in the order that you expect.
|
||||||
|
|
||||||
|
Thanks to:
|
||||||
|
http://code.activestate.com/recipes/66012-fork-a-daemon-process-on-unix/
|
||||||
|
"""
|
||||||
|
# Do first fork.
|
||||||
|
try:
|
||||||
|
pid = os.fork()
|
||||||
|
if pid > 0:
|
||||||
|
os.waitpid(pid, 0)
|
||||||
|
return 0 # Return 0 from first parent.
|
||||||
|
except OSError as e:
|
||||||
|
sys.stderr.write("fork #1 failed: (%d) %s\n" % (e.errno, e.strerror))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Decouple from parent environment.
|
||||||
|
os.chdir("/")
|
||||||
|
os.umask(0)
|
||||||
|
os.setsid()
|
||||||
|
|
||||||
|
# Do second fork.
|
||||||
|
try:
|
||||||
|
pid = os.fork()
|
||||||
|
if pid > 0:
|
||||||
|
sys.exit(0) # Exit second parent.
|
||||||
|
except OSError as e:
|
||||||
|
sys.stderr.write("fork #2 failed: (%d) %s\n" % (e.errno, e.strerror))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Now I am a daemon!
|
||||||
|
|
||||||
|
# Redirect standard file descriptors.
|
||||||
|
|
||||||
|
# NOTE: For debugging, you meight want to take these instead of /dev/null.
|
||||||
|
# so = open('/tmp/log2', 'ab+')
|
||||||
|
# se = open('/tmp/log2', 'ab+', 0)
|
||||||
|
|
||||||
|
si = open(stdin, 'rb')
|
||||||
|
so = open(stdout, 'ab+')
|
||||||
|
se = open(stderr, 'ab+', 0)
|
||||||
|
os.dup2(si.fileno(), sys.stdin.fileno())
|
||||||
|
os.dup2(so.fileno(), sys.stdout.fileno())
|
||||||
|
os.dup2(se.fileno(), sys.stderr.fileno())
|
||||||
|
|
||||||
|
# Return 1 from daemon.
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
class nonblocking(object):
|
||||||
|
"""
|
||||||
|
Make fd non blocking.
|
||||||
|
"""
|
||||||
|
def __init__(self, fd):
|
||||||
|
self.fd = fd
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
import fcntl
|
||||||
|
self.orig_fl = fcntl.fcntl(self.fd, fcntl.F_GETFL)
|
||||||
|
fcntl.fcntl(self.fd, fcntl.F_SETFL, self.orig_fl | os.O_NONBLOCK)
|
||||||
|
|
||||||
|
def __exit__(self, *args):
|
||||||
|
import fcntl
|
||||||
|
fcntl.fcntl(self.fd, fcntl.F_SETFL, self.orig_fl)
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_shell():
|
||||||
|
"""
|
||||||
|
return the path to the default shell for the current user.
|
||||||
|
"""
|
||||||
|
if is_windows():
|
||||||
|
return 'cmd.exe'
|
||||||
|
else:
|
||||||
|
import pwd
|
||||||
|
import getpass
|
||||||
|
|
||||||
|
if 'SHELL' in os.environ:
|
||||||
|
return os.environ['SHELL']
|
||||||
|
else:
|
||||||
|
username = getpass.getuser()
|
||||||
|
shell = pwd.getpwnam(username).pw_shell
|
||||||
|
return shell
|
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
prompt_toolkit
|
||||||
|
ptterm
|
||||||
|
six
|
||||||
|
docopt
|
Loading…
Reference in New Issue
Block a user