added chat overlay
This commit is contained in:
commit
3275444755
|
@ -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`
|
||||
---
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
from __future__ import unicode_literals
|
|
@ -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()
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
from __future__ import unicode_literals
|
||||
from .base import Client
|
||||
from .defaults import create_client, list_clients
|
|
@ -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.
|
||||
"""
|
|
@ -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()
|
|
@ -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
|
|
@ -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,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',
|
||||
}
|
|
@ -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
|
|
@ -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)
|
|
@ -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,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()
|
|
@ -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'
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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, ),
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1,9 @@
|
|||
from __future__ import unicode_literals
|
||||
import logging
|
||||
|
||||
__all__ = (
|
||||
'logger',
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__package__)
|
|
@ -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]
|
|
@ -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),
|
||||
}
|
|
@ -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)
|
|
@ -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. "
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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))
|
|
@ -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 '%%'"
|
||||
"""
|
|
@ -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()
|
|
@ -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)
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
prompt_toolkit
|
||||
ptterm
|
||||
six
|
||||
docopt
|
Loading…
Reference in New Issue