def __init__(self, title='', text='', ok_text='Ok', width=None, wrap_lines=True, scrollbar=False): self.future = Future() def accept_text(buf): get_app().layout.focus(ok_button) buf.complete_state = None return True def accept(): self.future.set_result(self.text_area.text) def cancel(): self.future.set_result(None) text_width = len(max(text.split('\n'), key=len)) + 2 self.text_area = TextArea(completer=completer, multiline=False, width=D(preferred=text_width), accept_handler=accept_text) ok_button = Button(text='OK', handler=accept) cancel_button = Button(text='Cancel', handler=cancel) self.dialog = Dialog(title=title, body=HSplit([Label(text=text), self.text_area]), buttons=[ok_button, cancel_button], width=width, modal=True)
def wait_for_cpr_responses(self, timeout=1): """ Wait for a CPR response. """ cpr_futures = list(self._waiting_for_cpr_futures) # Make copy. # When there are no CPRs in the queue. Don't do anything. if not cpr_futures or self.cpr_support == CPR_Support.NOT_SUPPORTED: return Future.succeed(None) f = Future() # When a CPR has been received, set the result. def wait_for_responses(): for response_f in cpr_futures: yield From(response_f) if not f.done(): f.set_result(None) ensure_future(wait_for_responses()) # Timeout. def wait_for_timeout(): time.sleep(timeout) # Got timeout. if not f.done(): self._waiting_for_cpr_futures = deque() f.set_result(None) t = threading.Thread(target=wait_for_timeout) t.daemon = True t.start() return f
def __init__(self, title='', label_text='', completer=None): self.future = Future() def accept_text(): get_app().layout.focus(ok_button) self.text_area.buffer.complete_state = None def accept(): self.future.set_result(self.text_area.text) def cancel(): self.future.set_result(None) self.text_area = TextArea( completer=completer, multiline=False, width=D(preferred=40), accept_handler=accept_text) ok_button = Button(text='OK', handler=accept) cancel_button = Button(text='Cancel', handler=cancel) self.dialog = Dialog( title=title, body=HSplit([ Label(text=label_text), self.text_area ]), buttons=[ok_button, cancel_button], width=D(preferred=80), modal=True)
def __init__(self, title='', text='', ok_text='Ok', lexer=None, width=None, wrap_lines=True, scrollbar=False): self.future = Future() self.text = text def set_done(): self.future.set_result(None) def get_text_width(): if width is None: text_fragments = to_formatted_text(self.text) text = fragment_list_to_text(text_fragments) if text: longest_line = max( get_cwidth(line) for line in text.splitlines()) else: return D(preferred=0) return D(preferred=longest_line) else: return width # text_width = len(max(self.text.split('\n'), key=len)) + 2 # text_height = len(text.split('\n')) # TODO: Add dynamic_h_scrollbar to TextArea and this Dialog # def dynamic_horizontal_scrollbar(): # max_text_width = get_app().renderer.output.get_size().columns - 2 def dynamic_virtical_scrollbar(): text_fragments = to_formatted_text(self.text) text = fragment_list_to_text(text_fragments) if text: text_height = len(self.text.splitlines()) max_text_height = get_app().renderer.output.get_size().rows - 6 if text_height > max_text_height: return True self.text_area = TextArea(text=text, lexer=lexer, read_only=True, focusable=False, width=get_text_width(), wrap_lines=wrap_lines, scrollbar=dynamic_virtical_scrollbar()) ok_button = Button(text='OK', handler=(lambda: set_done())) self.dialog = Dialog(title=title, body=self.text_area, buttons=[ok_button], width=width, modal=True)
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
def __init__(self, exec_func): self.exec_func = exec_func # Create pseudo terminal for this pane. self.master, self.slave = os.openpty() # Master side -> attached to terminal emulator. self._reader = PosixStdinReader(self.master, errors='replace') self._reader_connected = False self._input_ready_callbacks = [] self.ready_f = Future() self.loop = get_event_loop() self.pid = None
def _async_generator(): " Simple asynchronous generator. " # await. result = yield From(Future.succeed(1)) # yield yield AsyncGeneratorItem(result + 1) # await. result = yield From(Future.succeed(10)) # yield yield AsyncGeneratorItem(result + 1)
def __init__(self, title, text): self.future = Future() def set_done(): self.future.set_result(None) ok_button = Button(text='OK', handler=(lambda: set_done())) self.dialog = Dialog(title=title, body=HSplit([ Label(text=text), ]), buttons=[ok_button], width=D(preferred=80), modal=True)
def run(): with context() as ctx_id: self._context_id = ctx_id # Set input/output for all application running in this context. set_default_input(self.vt100_input) set_default_output(self.vt100_output) # Add reader. loop = get_event_loop() loop.add_reader(self.conn, handle_incoming_data) try: obj = self.interact(self) if _is_coroutine(obj): # Got an asyncio coroutine. import asyncio f = asyncio.ensure_future(obj) yield From(Future.from_asyncio_future(f)) else: # Got a prompt_toolkit coroutine. yield From(obj) except Exception as e: print('Got %s' % type(e).__name__, e) import traceback traceback.print_exc() raise finally: self.close()
def __init__(self, title='', text='', label='', values=[], padding=4, completer=None): self.future = Future() self.radios = RadioList(values=values) # radios.current_value will contain the first component of the selected tuple # title = "Delete" # values =[ # (0, 'this instance'), # (1, 'this and all subsequent instances'), # (2, 'this and all previous instances'), # (3, 'all instances - the item itself'), # ] def accept(): self.future.set_result(self.radios.current_value) def cancel(): self.future.set_result(None) ok_button = Button(text='OK', handler=accept) cancel_button = Button(text='Cancel', handler=cancel) self.dialog = Dialog( title=title, body=HSplit([ Label(text=text), Frame(title=label, body=self.radios) ]), # body= Frame(title=label, body=self.radios), buttons=[ok_button, cancel_button], width=D(preferred=shutil.get_terminal_size()[0]-10), modal=True)
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
def __init__(self, title='', label_text='', completer=None): self.future = Future() def accept_text(buf): get_app().layout.focus(ok_button) buf.complete_state = None return True def accept(): self.future.set_result(self.text_area.text) def cancel(): self.future.set_result(None) self.text_area = TextArea( completer=completer, multiline=False, width=D(preferred=40), accept_handler=accept_text) ok_button = Button(text='OK', handler=accept) cancel_button = Button(text='Cancel', handler=cancel) self.dialog = Dialog( title=title, body=HSplit([ Label(text=label_text), self.text_area ]), buttons=[ok_button, cancel_button], width=D(preferred=80), modal=True)
class InteractiveInputDialog(object): def __init__(self, title='', help_text='', evaluator=None, padding=10, completer=None): self.future = Future() def cancel(): self.future.set_result(None) self.output_field = TextArea( text='', focusable=False, ) self.input_field = TextArea( height=1, prompt='>>> ', multiline=False, focusable=True, wrap_lines=False) def accept(buff): # Evaluate "calculator" expression. try: output = 'In: {}\nOut: {}\n\n'.format( self.input_field.text, evaluator(self.input_field.text)) except BaseException as e: output = '\n\n{}'.format(e) new_text = self.output_field.text + output # Add text to output buffer. self.output_field.buffer.text = new_text self.input_field.accept_handler = accept cancel_button = Button(text='Cancel', handler=cancel) self.dialog = Dialog( title=title, body=HSplit([ Label(text=help_text), self.output_field, HorizontalLine(), self.input_field, ]), buttons=[cancel_button], width=D(preferred=shutil.get_terminal_size()[0]-padding), modal=True) def __pt_container__(self): return self.dialog
class YesNoDialog(object): def __init__(self, title='', text='', yes_text='Yes', no_text='No', width=None, wrap_lines=True, scrollbar=False): self.future = Future() def yes_handler(): self.future.set_result(True) def no_handler(): self.future.set_result(False) text_width = len(max(text.split('\n'), key=len)) + 2 self.text_area = TextArea( text=text, read_only=True, # focus_on_click = True, focusable=False, width=D(preferred=text_width), wrap_lines=wrap_lines, scrollbar=scrollbar) self.dialog = Dialog(title=title, body=self.text_area, buttons=[ Button(text=yes_text, width=1, handler=yes_handler), Button(text=no_text, width=1, handler=no_handler) ], with_background=True, modal=True) def __pt_container__(self): return self.dialog
def async_6(): " Create a `Future` and call `set_exception` later on. " f = Future() def in_executor(): time.sleep(.2) f.set_exception(Exception('Failure from async_6')) run_in_executor(in_executor) return f
def async_5(): " Create a `Future` and call `set_result` later on. " f = Future() def in_executor(): time.sleep(.2) f.set_result('Hello from async_5') run_in_executor(in_executor) return f
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
class MessageDialog(object): def __init__(self, title='', text='', ok_text='Ok', shadow=False, width=None, wrap_lines=True, scrollbar=False): self.future = Future() def set_done(): self.future.set_result(None) text_width = len(max(text.split('\n'), key=len)) + 2 text_height = len(text.split('\n')) app_height = get_app( ).ctui.layout.output_field.window.render_info.window_height - 2 def dynamic_scrollbar(): if text_height > app_height: return True self.text_area = TextArea( text=text, read_only=True, # focus_on_click = True, focusable=False, width=D(preferred=text_width), wrap_lines=wrap_lines, scrollbar=dynamic_scrollbar()) ok_button = Button(text='OK', handler=(lambda: set_done())) self.dialog = Dialog(title=title, body=self.text_area, buttons=[ok_button], width=width, modal=True) def __pt_container__(self): return self.dialog
class MessageDialog(object): def __init__(self, title, text): self.future = Future() def set_done(): self.future.set_result(None) ok_button = Button(text='OK', handler=(lambda: set_done())) self.dialog = Dialog( title=title, body=HSplit([ Label(text=text), ]), buttons=[ok_button], width=D(preferred=80), modal=True) def __pt_container__(self): return self.dialog
def _run_in_terminal(self, func): # Make sure that when an application was active for this connection, # that we print the text above the application. with context(self._context_id): try: app = get_app(raise_exception=True) except NoRunningApplicationError: func() return Future.succeed(None) else: return app.run_in_terminal(func)
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 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
class TextInputDialog(object): def __init__(self, title='', label_text='', default='', padding=10, completer=None): self.future = Future() def accept_text(buf): get_app().layout.focus(ok_button) buf.complete_state = None return True def accept(): self.future.set_result(self.text_area.text) def cancel(): self.future.set_result(None) self.text_area = TextArea( completer=completer, text=default, multiline=False, width=D(preferred=shutil.get_terminal_size()[0]-padding), accept_handler=accept_text) ok_button = Button(text='OK', handler=accept) cancel_button = Button(text='Cancel', handler=cancel) self.dialog = Dialog( title=title, body=HSplit([ Label(text=label_text), self.text_area ]), buttons=[ok_button, cancel_button], # buttons=[ok_button], width=D(preferred=shutil.get_terminal_size()[0]-10), modal=True) def __pt_container__(self): return self.dialog
class ConfirmDialog(object): def __init__(self, title="", text="", padding=10): self.future = Future() def set_yes(): self.future.set_result(True) def set_no(): self.future.set_result(False) yes_button = Button(text='Yes', handler=(lambda: set_yes())) no_button = Button(text='No', handler=(lambda: set_no())) self.dialog = Dialog( title=title, body=HSplit([ Label(text=text), ]), buttons=[yes_button, no_button], width=D(preferred=shutil.get_terminal_size()[0]-padding), modal=True) def __pt_container__(self): return self.dialog
def __init__(self, title="", text="", padding=10): self.future = Future() def set_done(): self.future.set_result(None) ok_button = Button(text='OK', handler=(lambda: set_done())) self.dialog = Dialog( title=title, body=HSplit([ Label(text=text), ]), buttons=[ok_button], width=D(preferred=shutil.get_terminal_size()[0]-padding), modal=True)
def request_absolute_cursor_position(self): """ Get current cursor position. For vt100: Do CPR request. (answer will arrive later.) For win32: Do API call. (Answer comes immediately.) """ # Only do this request when the cursor is at the top row. (after a # clear or reset). We will rely on that in `report_absolute_cursor_row`. assert self._cursor_pos.y == 0 # For Win32, we have an API call to get the number of rows below the # cursor. if is_windows(): self._min_available_height = self.output.get_rows_below_cursor_position( ) else: if self.full_screen: self._min_available_height = self.output.get_size().rows elif self.cpr_support == CPR_Support.NOT_SUPPORTED: return else: # Asks for a cursor position report (CPR). self._waiting_for_cpr_futures.append(Future()) self.output.ask_for_cpr() # If we don't know whether CPR is supported, test using timer. if self.cpr_support == CPR_Support.UNKNOWN: def timer(): time.sleep(self.CPR_TIMEOUT) # Not set in the meantime -> not supported. if self.cpr_support == CPR_Support.UNKNOWN: self.cpr_support = CPR_Support.NOT_SUPPORTED if self.cpr_not_supported_callback: # Make sure to call this callback in the main thread. get_event_loop().call_from_executor( self.cpr_not_supported_callback) t = threading.Thread(target=timer) t.daemon = True t.start()
def __init__(self, mic, recognizer): self.future = future = Future() def listen(): with mic as source: recognizer.adjust_for_ambient_noise(source, 0.5) audio = recognizer.listen(source) future.set_result(audio) run_in_executor(listen) self.dialog = Dialog(title='LISTENING', body=HSplit([ TextArea(text='', multiline=False, read_only=True), ]), width=D(preferred=80), modal=True)
def __init__(self, choices, title=''): self.future = future = Future() text = 'Did you mean one of the following?' buttons = [] for no, cmd in enumerate(choices): text += '\n{no}. {cmd}'.format(no=no, cmd=cmd) buttons.append( Button(text=str(no), handler=lambda i=no: future.set_result(i))) buttons.append( Button(text='none', handler=lambda: future.set_result(None))) self.dialog = Dialog(title=title, body=HSplit([ Label(text=text), ]), buttons=buttons, width=D(preferred=80), modal=True)
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 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
def __init__(self, pipe_instance): assert isinstance(pipe_instance, PipeInstance) self.pipe_instance = pipe_instance self.done_f = Future()
def async_3(): " Succeed immediately. " return Future.succeed('Hello from async_3')
def do_cpr(): # Asks for a cursor position report (CPR). self._waiting_for_cpr_futures.append(Future()) self.output.ask_for_cpr()
def other_coroutine(): value = yield From(Future.succeed(True)) value = yield From(Future.succeed(True)) raise Return('Result from coroutine.')
def async_4(): " Fail immediately. " return Future.fail(Exception('Failure from async_4'))
def async_func(): result = func() return Future.succeed(result)
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 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: 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(): 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: 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_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]
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.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 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: 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(): 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: 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_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]
class PosixTerminal(Terminal): def __init__(self, exec_func): self.exec_func = exec_func # Create pseudo terminal for this pane. self.master, self.slave = os.openpty() # Master side -> attached to terminal emulator. self._reader = PosixStdinReader(self.master, errors='replace') self._reader_connected = False self._input_ready_callbacks = [] self.ready_f = Future() self.loop = get_event_loop() self.pid = None def add_input_ready_callback(self, callback): self._input_ready_callbacks.append(callback) @classmethod def from_command(cls, command, before_exec_func=None): """ Create Process from command, e.g. command=['python', '-c', 'print("test")'] :param before_exec_func: Function that is called before `exec` in the process fork. """ assert isinstance(command, list) assert before_exec_func is None or callable(before_exec_func) def execv(): if before_exec_func: before_exec_func() for p in os.environ['PATH'].split(':'): path = os.path.join(p, command[0]) if os.path.exists(path) and os.access(path, os.X_OK): os.execv(path, command) return cls(execv) def connect_reader(self): if self.master is not None and not self._reader_connected: def ready(): for cb in self._input_ready_callbacks: cb() self.loop.add_reader(self.master, ready) self._reader_connected = True @property def closed(self): return self._reader.closed def disconnect_reader(self): if self.master is not None and self._reader_connected: self.loop.remove_reader(self.master) self._reader_connected = False def read_text(self, amount=4096): return self._reader.read(amount) def write_text(self, text): self.write_bytes(text.encode('utf-8')) def write_bytes(self, data): while self.master is not None: try: os.write(self.master, data) except OSError as e: # This happens when the window resizes and a SIGWINCH was received. # We get 'Error: [Errno 4] Interrupted system call' if e.errno == 4: continue return def set_size(self, width, height): """ Set terminal size. """ assert isinstance(width, int) assert isinstance(height, int) if self.master is not None: set_terminal_size(self.master, height, width) def start(self): """ Create fork and start the child process. """ pid = os.fork() if pid == 0: self._in_child() elif pid > 0: # We wait a very short while, to be sure the child had the time to # call _exec. (Otherwise, we are still sharing signal handlers and # FDs.) Resizing the pty, when the child is still in our Python # code and has the signal handler from prompt_toolkit, but closed # the 'fd' for 'call_from_executor', will cause OSError. time.sleep(0.1) self.pid = pid # Wait for the process to finish. self._waitpid() def kill(self): " Terminate process. " self.send_signal(signal.SIGKILL) def send_signal(self, signal): " Send signal to running process. " assert isinstance(signal, int), type(signal) if self.pid and not self.closed: try: os.kill(self.pid, signal) except OSError: pass # [Errno 3] No such process. def _in_child(self): " Will be executed in the forked child. " os.close(self.master) # Remove signal handler for SIGWINCH as early as possible. # (We don't want this to be triggered when execv has not been called # yet.) signal.signal(signal.SIGWINCH, 0) pty_make_controlling_tty(self.slave) # In the fork, set the stdin/out/err to our slave pty. os.dup2(self.slave, 0) os.dup2(self.slave, 1) os.dup2(self.slave, 2) # Execute in child. try: self._close_file_descriptors() self.exec_func() except Exception: traceback.print_exc() time.sleep(5) os._exit(1) os._exit(0) def _close_file_descriptors(self): # Do not allow child to inherit open file descriptors from parent. # (In case that we keep running Python code. We shouldn't close them. # because the garbage collector is still active, and he will close them # eventually.) max_fd = resource.getrlimit(resource.RLIMIT_NOFILE)[-1] try: os.closerange(3, max_fd) except OverflowError: # On OS X, max_fd can return very big values, than closerange # doesn't understand, e.g. 9223372036854775807. In this case, just # use 4096. This is what Linux systems report, and should be # sufficient. (I hope...) os.closerange(3, 4096) def _waitpid(self): """ Create an executor that waits and handles process termination. """ def wait_for_finished(): " Wait for PID in executor. " os.waitpid(self.pid, 0) self.loop.call_from_executor(done) def done(): " PID received. Back in the main thread. " # Close pty and remove reader. self.disconnect_reader() os.close(self.master) os.close(self.slave) self.master = None # Callback. self.ready_f.set_result(None) self.loop.run_in_executor(wait_for_finished) def get_name(self): " Return the process name. " result = '<unknown>' # Apparently, on a Linux system (like my Fedora box), I have to call # `tcgetpgrp` on the `master` fd. However, on te Window subsystem for # Linux, we have to use the `slave` fd. if self.master is not None: result = get_name_for_fd(self.master) if not result and self.slave is not None: result = get_name_for_fd(self.slave) return result def get_cwd(self): if self.pid: return get_cwd_for_pid(self.pid)