def run(self, stdin, callbacks): inputstream = InputStream(callbacks.feed_key) stdin_reader = PosixStdinReader(stdin.fileno()) self._callbacks = callbacks if in_main_thread(): ctx = call_on_sigwinch(self.received_winch) else: ctx = DummyContext() select_timeout = INPUT_TIMEOUT with ctx: while self._running: r, _, _ = select.select( [stdin.fileno(), self._schedule_pipe_read], [], [], select_timeout) if stdin.fileno() in r: select_timeout = INPUT_TIMEOUT data = stdin_reader.read() inputstream.feed(data) if stdin_reader.closed: break elif self._schedule_pipe_read in r: os.read(self._schedule_pipe_read, 8192) while True: try: task = self._calls_from_executor.pop(0) except IndexError: break else: task() else: # timeout inputstream.flush() callbacks.input_timeout() select_timeout = None self._callbacks = None
def run(self, stdin, callbacks): inputstream = InputStream(callbacks.feed_key) stdin_reader = PosixStdinReader(stdin.fileno()) self._callbacks = callbacks if in_main_thread(): ctx = call_on_sigwinch(self.received_winch) else: ctx = DummyContext() select_timeout = INPUT_TIMEOUT with ctx: while self._running: r, _, _ = select.select([stdin.fileno(),self._schedule_pipe_read], [], [],select_timeout) if stdin.fileno() in r: select_timeout = INPUT_TIMEOUT data = stdin_reader.read() inputstream.feed(data) if stdin_reader.closed: break elif self._schedule_pipe_read in r: os.read(self._schedule_pipe_read,8192) while True: try: task = self._calls_from_executor.pop(0) except IndexError: break else: task() else: # timeout inputstream.flush() callbacks.input_timeout() select_timeout = None self._callbacks = None
class Process(object): """ Child process. Functionality for parsing the vt100 output (the Pyte screen and stream), as well as sending input to the process. Usage: p = Process(eventloop, ...): p.start() :param eventloop: Prompt_toolkit eventloop. Used for executing blocking stuff in an executor, as well as adding additional readers to the eventloop. :param invalidate: When the screen content changes, and the renderer needs to redraw the output, this callback is called. :param exec_func: Callable that is called in the child process. (Usualy, this calls execv.) :param bell_func: Called when the process does a `bell`. :param done_callback: Called when the process terminates. :param has_priority: Callable that returns True when this Process should get priority in the event loop. (When this pane has the focus.) Otherwise output can be delayed. """ def __init__(self, eventloop, invalidate, exec_func, bell_func=None, done_callback=None, has_priority=None): assert isinstance(eventloop, EventLoop) assert callable(invalidate) assert callable(exec_func) assert bell_func is None or callable(bell_func) assert done_callback is None or callable(done_callback) assert has_priority is None or callable(has_priority) self.eventloop = eventloop self.invalidate = invalidate self.exec_func = exec_func self.done_callback = done_callback self.has_priority = has_priority or (lambda: True) self.pid = None self.is_terminated = False self.suspended = False # Create pseudo terminal for this pane. self.master, self.slave = os.openpty() # Master side -> attached to terminal emulator. self._reader = PosixStdinReader(self.master) # Create output stream and attach to screen self.sx = 0 self.sy = 0 self.screen = BetterScreen(self.sx, self.sy, write_process_input=self.write_input, bell_func=bell_func) self.stream = BetterStream(self.screen) self.stream.attach(self.screen) def start(self): """ Start the process: fork child. """ self.set_size(120, 24) self._start() self._connect_reader() self._waitpid() @classmethod def from_command(cls, eventloop, invalidate, command, done_callback, bell_func=None, before_exec_func=None, has_priority=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) 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(eventloop, invalidate, execv, bell_func=bell_func, done_callback=done_callback, has_priority=has_priority) def _start(self): """ Create fork and start the child process. """ pid = os.fork() if pid == 0: self._in_child() elif pid > 0: # In parent. os.close(self.slave) self.slave = None # 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 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.eventloop.call_from_executor(done) def done(): " PID received. Back in the main thread. " # Close pty and remove reader. os.close(self.master) self.eventloop.remove_reader(self.master) self.master = None # Callback. self.is_terminated = True self.done_callback() self.eventloop.run_in_executor(wait_for_finished) def set_size(self, width, height): """ Set terminal size. """ assert isinstance(width, int) assert isinstance(height, int) if self.master is not None: if (self.sx, self.sy) != (width, height): set_terminal_size(self.master, height, width) self.screen.resize(lines=height, columns=width) self.screen.lines = height self.screen.columns = width self.sx = width self.sy = height 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 write_input(self, data, paste=False): """ Write user key strokes to the input. :param data: (text, not bytes.) The input. :param paste: When True, and the process running here understands bracketed paste. Send as pasted text. """ # send as bracketed paste? if paste and self.screen.bracketed_paste_enabled: data = '\x1b[200~' + data + '\x1b[201~' while self.master is not None: try: os.write(self.master, data.encode('utf-8')) 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 write_key(self, key): """ Write prompt_toolkit Key. """ data = prompt_toolkit_key_to_vt100_key( key, application_mode=self.screen.in_application_mode) self.write_input(data) def _connect_reader(self): """ Process stdout output from the process. """ if self.master is not None: self.eventloop.add_reader(self.master, self._read) def _read(self): """ Read callback, called by the eventloop. """ d = self._reader.read( 4096) # Make sure not to read too much at once. (Otherwise, # this could block the event loop.) if d: def process(): self.stream.feed(d) self.invalidate() # Feed directly, if this process has priority. (That is when this # pane has the focus in any of the clients.) if self.has_priority(): process() # Otherwise, postpone processing until we have CPU time available. else: self.eventloop.remove_reader(self.master) def do_asap(): " Process output and reconnect to event loop. " process() self._connect_reader() # When the event loop is saturated because of CPU, we will # postpone this processing max 'x' seconds. # '1' seems like a reasonable value, because that way we say # that we will process max 1k/1s in case of saturation. # That should be enough to prevent the UI from feeling # unresponsive. timestamp = datetime.datetime.now() + datetime.timedelta( seconds=1) self.eventloop.call_from_executor( do_asap, _max_postpone_until=timestamp) else: # End of stream. Remove child. self.eventloop.remove_reader(self.master) def suspend(self): """ Suspend process. Stop reading stdout. (Called when going into copy mode.) """ self.suspended = True self.eventloop.remove_reader(self.master) def resume(self): """ Resume from 'suspend'. """ if self.suspended and self.master is not None: self._connect_reader() self.suspended = False def get_cwd(self): """ The current working directory for this process. (Or `None` when unknown.) """ return get_cwd_for_pid(self.pid) def get_name(self): """ The name for this process. (Or `None` when unknown.) """ # TODO: Maybe cache for short time. if self.master is not None: return get_name_for_fd(self.master) def send_signal(self, signal): " Send signal to running process. " assert isinstance(signal, int), type(signal) if self.pid and not self.is_terminated: os.kill(self.pid, signal) def create_copy_document(self): """ Create a Document instance and token list that can be used in copy mode. """ data_buffer = self.screen.pt_screen.data_buffer text = [] token_list = [] first_row = min(data_buffer.keys()) last_row = max(data_buffer.keys()) def token_has_no_background(token): try: # Token looks like ('C', color, bgcolor, bold, underline, ...) return token[2] is None except IndexError: return True for row_index in range(first_row, last_row + 1): row = data_buffer[row_index] max_column = max(row.keys()) if row else 0 # Remove trailing whitespace. (If the background is transparent.) row_data = [row[x] for x in range(0, max_column + 1)] while (row_data and row_data[-1].char.isspace() and token_has_no_background(row_data[-1].token)): row_data.pop() # Walk through row. char_iter = iter(range(len(row_data))) for x in char_iter: c = row[x] text.append(c.char) token_list.append((c.token, c.char)) # Skip next cell when this is a double width character. if c.width == 2: next(char_iter) # Add newline. text.append('\n') token_list.append((Token, '\n')) # Remove newlines at the end. while text and text[-1] == '\n': text.pop() token_list.pop() # Calculate cursor position. d = Document(text=''.join(text)) return Document( text=d.text, cursor_position=d.translate_row_col_to_index( row=self.screen.pt_screen.cursor_position.y, col=self.screen.pt_screen.cursor_position.x)), token_list
class Client(object): 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, true_color=False): """ Attach client user interface. """ assert isinstance(detach_other_clients, bool) assert isinstance(true_color, bool) self._send_size() self._send_packet({ 'cmd': 'start-gui', 'detach-others': detach_other_clients, 'true-color': true_color, '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. with call_on_sigwinch(self._send_size): while True: r, w, x = _select([stdin_fd, socket_fd], [], [], current_timeout) if socket_fd in r: # Received packet from server. data = self.socket.recv(1024) 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 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]})
class Process(object): """ Child process. Functionality for parsing the vt100 output (the Pyte screen and stream), as well as sending input to the process. Usage: p = Process(eventloop, ...): p.start() :param eventloop: Prompt_toolkit eventloop. Used for executing blocking stuff in an executor, as well as adding additional readers to the eventloop. :param invalidate: When the screen content changes, and the renderer needs to redraw the output, this callback is called. :param exec_func: Callable that is called in the child process. (Usualy, this calls execv.) :param bell_func: Called when the process does a `bell`. :param done_callback: Called when the process terminates. :param has_priority: Callable that returns True when this Process should get priority in the event loop. (When this pane has the focus.) Otherwise output can be delayed. """ def __init__(self, eventloop, invalidate, exec_func, bell_func=None, done_callback=None, has_priority=None): assert isinstance(eventloop, EventLoop) assert callable(invalidate) assert callable(exec_func) assert bell_func is None or callable(bell_func) assert done_callback is None or callable(done_callback) assert has_priority is None or callable(has_priority) self.eventloop = eventloop self.invalidate = invalidate self.exec_func = exec_func self.done_callback = done_callback self.has_priority = has_priority or (lambda: True) self.pid = None self.is_terminated = False self.suspended = False # 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') # Create output stream and attach to screen self.sx = 0 self.sy = 0 self.screen = BetterScreen( self.sx, self.sy, write_process_input=self.write_input, bell_func=bell_func) self.stream = BetterStream(self.screen) self.stream.attach(self.screen) def start(self): """ Start the process: fork child. """ self.set_size(120, 24) self._start() self._connect_reader() self._waitpid() @classmethod def from_command(cls, eventloop, invalidate, command, done_callback, bell_func=None, before_exec_func=None, has_priority=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) 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( eventloop, invalidate, execv, bell_func=bell_func, done_callback=done_callback, has_priority=has_priority) def _start(self): """ Create fork and start the child process. """ pid = os.fork() if pid == 0: self._in_child() elif pid > 0: # In parent. os.close(self.slave) self.slave = None # 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 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.eventloop.call_from_executor(done) def done(): " PID received. Back in the main thread. " # Close pty and remove reader. os.close(self.master) self.eventloop.remove_reader(self.master) self.master = None # Callback. self.is_terminated = True self.done_callback() self.eventloop.run_in_executor(wait_for_finished) def set_size(self, width, height): """ Set terminal size. """ assert isinstance(width, int) assert isinstance(height, int) if self.master is not None: if (self.sx, self.sy) != (width, height): set_terminal_size(self.master, height, width) self.screen.resize(lines=height, columns=width) self.screen.lines = height self.screen.columns = width self.sx = width self.sy = height 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 write_input(self, data, paste=False): """ Write user key strokes to the input. :param data: (text, not bytes.) The input. :param paste: When True, and the process running here understands bracketed paste. Send as pasted text. """ # send as bracketed paste? if paste and self.screen.bracketed_paste_enabled: data = '\x1b[200~' + data + '\x1b[201~' self.write_bytes(data.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 write_key(self, key): """ Write prompt_toolkit Key. """ data = prompt_toolkit_key_to_vt100_key( key, application_mode=self.screen.in_application_mode) self.write_input(data) def _connect_reader(self): """ Process stdout output from the process. """ if self.master is not None: self.eventloop.add_reader(self.master, self._read) def _read(self): """ Read callback, called by the eventloop. """ d = self._reader.read( 4096) # Make sure not to read too much at once. (Otherwise, # this could block the event loop.) if not self._reader.closed: def process(): self.stream.feed(d) self.invalidate() # Feed directly, if this process has priority. (That is when this # pane has the focus in any of the clients.) if self.has_priority(): process() # Otherwise, postpone processing until we have CPU time available. else: self.eventloop.remove_reader(self.master) def do_asap(): " Process output and reconnect to event loop. " process() self._connect_reader() # When the event loop is saturated because of CPU, we will # postpone this processing max 'x' seconds. # '1' seems like a reasonable value, because that way we say # that we will process max 1k/1s in case of saturation. # That should be enough to prevent the UI from feeling # unresponsive. timestamp = datetime.datetime.now() + datetime.timedelta( seconds=1) self.eventloop.call_from_executor( do_asap, _max_postpone_until=timestamp) else: # End of stream. Remove child. self.eventloop.remove_reader(self.master) def suspend(self): """ Suspend process. Stop reading stdout. (Called when going into copy mode.) """ self.suspended = True self.eventloop.remove_reader(self.master) def resume(self): """ Resume from 'suspend'. """ if self.suspended and self.master is not None: self._connect_reader() self.suspended = False def get_cwd(self): """ The current working directory for this process. (Or `None` when unknown.) """ return get_cwd_for_pid(self.pid) def get_name(self): """ The name for this process. (Or `None` when unknown.) """ # TODO: Maybe cache for short time. if self.master is not None: return get_name_for_fd(self.master) def send_signal(self, signal): " Send signal to running process. " assert isinstance(signal, int), type(signal) if self.pid and not self.is_terminated: try: os.kill(self.pid, signal) except OSError: pass # [Errno 3] No such process. def create_copy_document(self): """ Create a Document instance and token list that can be used in copy mode. """ data_buffer = self.screen.data_buffer text = [] token_lists = [] first_row = min(data_buffer.keys()) last_row = max(data_buffer.keys()) def token_has_no_background(token): try: # Token looks like ('C', color, bgcolor, bold, underline, ...) return token[2] is None except IndexError: return True for lineno in range(first_row, last_row + 1): token_list = [] row = data_buffer[lineno] max_column = max(row.keys()) if row else 0 # Remove trailing whitespace. (If the background is transparent.) row_data = [row[x] for x in range(0, max_column + 1)] while (row_data and row_data[-1].char.isspace() and token_has_no_background(row_data[-1].token)): row_data.pop() # Walk through row. char_iter = iter(range(len(row_data))) for x in char_iter: c = row[x] text.append(c.char) token_list.append((c.token, c.char)) # Skip next cell when this is a double width character. if c.width == 2: try: next(char_iter) except StopIteration: pass token_lists.append(token_list) text.append('\n') def get_tokens_for_line(lineno): try: return token_lists[lineno] except IndexError: return [] # Calculate cursor position. d = Document(text=''.join(text)) return Document( text=d.text, cursor_position=d.translate_row_col_to_index( row=self.screen.pt_screen.cursor_position.y, col=self.screen.pt_screen.cursor_position. x)), get_tokens_for_line
class Client(object): 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, true_color=False): """ Attach client user interface. """ assert isinstance(detach_other_clients, bool) assert isinstance(true_color, bool) self._send_size() self._send_packet({ 'cmd': 'start-gui', 'detach-others': detach_other_clients, 'true-color': true_color, '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. with call_on_sigwinch(self._send_size): while True: r, w, x = _select([stdin_fd, socket_fd], [], [], current_timeout) if socket_fd in r: # Received packet from server. data = self.socket.recv(1024) 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 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] })
class PipeSource(Source): """ When input is read from another process that is chained to use through a unix pipe. """ def __init__(self, fileno, lexer=None, name='<stdin>'): assert isinstance(fileno, int) assert lexer is None or isinstance(lexer, Lexer) assert isinstance(name, six.text_type) self.fileno = fileno self.lexer = lexer self.name = name self._line_tokens = [] self._eof = False # Default style attributes. self._attrs = Attrs( color=None, bgcolor=None, bold=False, underline=False, italic=False, blink=False, reverse=False) # Start input parser. self._parser = self._parse_corot() next(self._parser) self._stdin_reader = PosixStdinReader(fileno) def get_name(self): return self.name def get_fd(self): return self.fileno def eof(self): return self._eof def read_chunk(self): # Content is ready for reading on stdin. data = self._stdin_reader.read() if not data: self._eof = True # Send input data to the parser. for c in data: self._parser.send(c) # Return the tokens from the parser. # (Don't return the last token yet, because the parser should # be able to pop if the input starts with \b). if self._eof: tokens = self._line_tokens[:] del self._line_tokens[:] else: tokens = self._line_tokens[:-1] del self._line_tokens[:-1] return tokens def _parse_corot(self): """ Coroutine that parses the pager input. A \b with any character before should make the next character standout. A \b with an underscore before should make the next character emphasized. """ token = Token line_tokens = self._line_tokens replace_one_token = False while True: csi = False c = yield if c == '\b': # Handle \b escape codes from man pages. if line_tokens: _, last_char = line_tokens[-1] line_tokens.pop() replace_one_token = True if last_char == '_': token = Token.Standout2 else: token = Token.Standout continue elif c == '\x1b': # Start of color escape sequence. square_bracket = yield if square_bracket == '[': csi = True else: continue elif c == '\x9b': csi = True if csi: # Got a CSI sequence. Color codes are following. current = '' params = [] while True: char = yield if char.isdigit(): current += char else: params.append(min(int(current or 0), 9999)) if char == ';': current = '' elif char == 'm': # Set attributes and token. self._select_graphic_rendition(params) token = ('C', ) + self._attrs break else: # Ignore unspported sequence. break else: line_tokens.append((token, c)) if replace_one_token: token = Token def _select_graphic_rendition(self, attrs): """ Taken a list of graphics attributes and apply changes to Attrs. """ # NOTE: This function is almost literally taken from Pymux. # if something is wrong, please report there as well! # https://github.com/jonathanslenders/pymux replace = {} if not attrs: attrs = [0] else: attrs = list(attrs[::-1]) while attrs: attr = attrs.pop() if attr in _fg_colors: replace["color"] = _fg_colors[attr] elif attr in _bg_colors: replace["bgcolor"] = _bg_colors[attr] elif attr == 1: replace["bold"] = True elif attr == 3: replace["italic"] = True elif attr == 4: replace["underline"] = True elif attr == 5: replace["blink"] = True elif attr == 6: replace["blink"] = True # Fast blink. elif attr == 7: replace["reverse"] = True elif attr == 22: replace["bold"] = False elif attr == 23: replace["italic"] = False elif attr == 24: replace["underline"] = False elif attr == 25: replace["blink"] = False elif attr == 27: replace["reverse"] = False elif not attr: replace = {} self._attrs = Attrs( color=None, bgcolor=None, bold=False, underline=False, italic=False, blink=False, reverse=False) elif attr in (38, 48): n = attrs.pop() # 256 colors. if n == 5: if attr == 38: m = attrs.pop() replace["color"] = _256_colors.get(1024 + m) elif attr == 48: m = attrs.pop() replace["bgcolor"] = _256_colors.get(1024 + m) # True colors. if n == 2: try: color_str = '%02x%02x%02x' % (attrs.pop(), attrs.pop(), attrs.pop()) except IndexError: pass else: if attr == 38: replace["color"] = color_str elif attr == 48: replace["bgcolor"] = color_str self._attrs = self._attrs._replace(**replace)