async def _process_stdout_tty(root: Root, stream: StdStream, stdout: Output, helper: AttachHelper) -> None: codec_info = codecs.lookup("utf8") decoder = codec_info.incrementaldecoder("replace") while True: chunk = await stream.read_out() if chunk is None: txt = decoder.decode(b"", final=True) if not txt: return else: txt = decoder.decode(chunk.data) async with helper.write_sem: if not helper.quiet and not helper.attach_ready: # Print header to stdout only, # logs are printed to stdout and never to # stderr (but logs printing is stopped by # helper.attach_ready = True regardless # what stream had receive text in attached mode. if helper.log_printed: s = ATTACH_STARTED_AFTER_LOGS if root.tty: s = click.style("√ ", fg="green") + s stdout.write_raw(s + "\n") helper.attach_ready = True stdout.write_raw(txt) stdout.flush()
async def _process_resizing(resizer: Callable[..., Awaitable[None]], stdout: Output) -> None: loop = asyncio.get_event_loop() resize_event = asyncio.Event() def resize() -> None: resize_event.set() has_sigwinch = (hasattr(signal, "SIGWINCH") and threading.current_thread() is threading.main_thread()) if has_sigwinch: previous_winch_handler = signal.getsignal(signal.SIGWINCH) loop.add_signal_handler(signal.SIGWINCH, resize) if previous_winch_handler is None: # Borrowed from the Prompt Toolkit. # In some situations we receive `None`. This is # however not a valid value for passing to # `signal.signal` at the end of this block. previous_winch_handler = signal.SIG_DFL prevh = prevw = None try: while True: if has_sigwinch: await resize_event.wait() resize_event.clear() else: # Windows or non-main thread # The logic is borrowed from docker CLI. # Wait for 250 ms # If there is no resize event -- check the size anyway on timeout. # It makes resizing to work on Windows. await asyncio.sleep(0.25) h, w = stdout.get_size() if prevh != h or prevw != w: prevh = h prevw = w await resizer(w=w, h=h) finally: if has_sigwinch: loop.remove_signal_handler(signal.SIGWINCH) signal.signal(signal.SIGWINCH, previous_winch_handler)
def print_formatted_text( output: Output, formatted_text: AnyFormattedText, style: BaseStyle, style_transformation: Optional[StyleTransformation] = None, color_depth: Optional[ColorDepth] = None) -> None: """ Print a list of (style_str, text) tuples in the given style to the output. """ fragments = to_formatted_text(formatted_text) style_transformation = style_transformation or DummyStyleTransformation() color_depth = color_depth or ColorDepth.default() # Reset first. output.reset_attributes() output.enable_autowrap() # Print all (style_str, text) tuples. attrs_for_style_string = _StyleStringToAttrsCache( style.get_attrs_for_style_str, style_transformation) for style_str, text, *_ in fragments: attrs = attrs_for_style_string[style_str] if attrs: output.set_attributes(attrs, color_depth) else: output.reset_attributes() # Assume that the output is raw, and insert a carriage return before # every newline. (Also important when the front-end is a telnet client.) assert '\r' not in text output.write(text.replace('\n', '\r\n')) # Reset again. output.reset_attributes() output.flush()
def _output_screen_diff( app: 'Application[Any]', output: Output, screen: Screen, current_pos: Point, color_depth: ColorDepth, previous_screen: Optional[Screen], last_style: Optional[str], is_done: bool, # XXX: drop is_done full_screen: bool, attrs_for_style_string: '_StyleStringToAttrsCache', size: Size, previous_width: int) -> Tuple[Point, Optional[str]]: """ Render the diff between this screen and the previous screen. This takes two `Screen` instances. The one that represents the output like it was during the last rendering and one that represents the current output raster. Looking at these two `Screen` instances, this function will render the difference by calling the appropriate methods of the `Output` object that only paint the changes to the terminal. This is some performance-critical code which is heavily optimized. Don't change things without profiling first. :param current_pos: Current cursor position. :param last_style: The style string, used for drawing the last drawn character. (Color/attributes.) :param attrs_for_style_string: :class:`._StyleStringToAttrsCache` instance. :param width: The width of the terminal. :param previous_width: The width of the terminal during the last rendering. """ width, height = size.columns, size.rows #: Variable for capturing the output. write = output.write write_raw = output.write_raw # Create locals for the most used output methods. # (Save expensive attribute lookups.) _output_set_attributes = output.set_attributes _output_reset_attributes = output.reset_attributes _output_cursor_forward = output.cursor_forward _output_cursor_up = output.cursor_up _output_cursor_backward = output.cursor_backward # Hide cursor before rendering. (Avoid flickering.) output.hide_cursor() def reset_attributes() -> None: " Wrapper around Output.reset_attributes. " nonlocal last_style _output_reset_attributes() last_style = None # Forget last char after resetting attributes. def move_cursor(new: Point) -> Point: " Move cursor to this `new` point. Returns the given Point. " current_x, current_y = current_pos.x, current_pos.y if new.y > current_y: # Use newlines instead of CURSOR_DOWN, because this might add new lines. # CURSOR_DOWN will never create new lines at the bottom. # Also reset attributes, otherwise the newline could draw a # background color. reset_attributes() write('\r\n' * (new.y - current_y)) current_x = 0 _output_cursor_forward(new.x) return new elif new.y < current_y: _output_cursor_up(current_y - new.y) if current_x >= width - 1: write('\r') _output_cursor_forward(new.x) elif new.x < current_x or current_x >= width - 1: _output_cursor_backward(current_x - new.x) elif new.x > current_x: _output_cursor_forward(new.x - current_x) return new def output_char(char: Char) -> None: """ Write the output of this character. """ nonlocal last_style # If the last printed character has the same style, don't output the # style again. if last_style == char.style: write(char.char) else: # Look up `Attr` for this style string. Only set attributes if different. # (Two style strings can still have the same formatting.) # Note that an empty style string can have formatting that needs to # be applied, because of style transformations. new_attrs = attrs_for_style_string[char.style] if not last_style or new_attrs != attrs_for_style_string[last_style]: _output_set_attributes(new_attrs, color_depth) write(char.char) last_style = char.style # Render for the first time: reset styling. if not previous_screen: reset_attributes() # Disable autowrap. (When entering a the alternate screen, or anytime when # we have a prompt. - In the case of a REPL, like IPython, people can have # background threads, and it's hard for debugging if their output is not # wrapped.) if not previous_screen or not full_screen: output.disable_autowrap() # When the previous screen has a different size, redraw everything anyway. # Also when we are done. (We might take up less rows, so clearing is important.) if is_done or not previous_screen or previous_width != width: # XXX: also consider height?? current_pos = move_cursor(Point(x=0, y=0)) reset_attributes() output.erase_down() previous_screen = Screen() # Get height of the screen. # (height changes as we loop over data_buffer, so remember the current value.) # (Also make sure to clip the height to the size of the output.) current_height = min(screen.height, height) # Loop over the rows. row_count = min(max(screen.height, previous_screen.height), height) c = 0 # Column counter. for y in range(row_count): new_row = screen.data_buffer[y] previous_row = previous_screen.data_buffer[y] zero_width_escapes_row = screen.zero_width_escapes[y] new_max_line_len = min(width - 1, max(new_row.keys()) if new_row else 0) previous_max_line_len = min(width - 1, max(previous_row.keys()) if previous_row else 0) # Loop over the columns. c = 0 while c < new_max_line_len + 1: new_char = new_row[c] old_char = previous_row[c] char_width = (new_char.width or 1) # When the old and new character at this position are different, # draw the output. (Because of the performance, we don't call # `Char.__ne__`, but inline the same expression.) if new_char.char != old_char.char or new_char.style != old_char.style: current_pos = move_cursor(Point(x=c, y=y)) # Send injected escape sequences to output. if c in zero_width_escapes_row: write_raw(zero_width_escapes_row[c]) output_char(new_char) current_pos = Point(x=current_pos.x + char_width, y=current_pos.y) c += char_width # If the new line is shorter, trim it. if previous_screen and new_max_line_len < previous_max_line_len: current_pos = move_cursor(Point(x=new_max_line_len + 1, y=y)) reset_attributes() output.erase_end_of_line() # Correctly reserve vertical space as required by the layout. # When this is a new screen (drawn for the first time), or for some reason # higher than the previous one. Move the cursor once to the bottom of the # output. That way, we're sure that the terminal scrolls up, even when the # lower lines of the canvas just contain whitespace. # The most obvious reason that we actually want this behaviour is the avoid # the artifact of the input scrolling when the completion menu is shown. # (If the scrolling is actually wanted, the layout can still be build in a # way to behave that way by setting a dynamic height.) if current_height > previous_screen.height: current_pos = move_cursor(Point(x=0, y=current_height - 1)) # Move cursor: if is_done: current_pos = move_cursor(Point(x=0, y=current_height)) output.erase_down() else: current_pos = move_cursor( screen.get_cursor_position(app.layout.current_window)) if is_done or not full_screen: output.enable_autowrap() # Always reset the color attributes. This is important because a background # thread could print data to stdout and we want that to be displayed in the # default colors. (Also, if a background color has been set, many terminals # give weird artifacts on resize events.) reset_attributes() if screen.show_cursor or is_done: output.show_cursor() return current_pos, last_style
def print_formatted_text( output: Output, formatted_text: AnyFormattedText, style: BaseStyle, style_transformation: Optional[StyleTransformation] = None, color_depth: Optional[ColorDepth] = None, ) -> None: """ Print a list of (style_str, text) tuples in the given style to the output. """ fragments = to_formatted_text(formatted_text) style_transformation = style_transformation or DummyStyleTransformation() color_depth = color_depth or output.get_default_color_depth() # Reset first. output.reset_attributes() output.enable_autowrap() last_attrs: Optional[Attrs] = None # Print all (style_str, text) tuples. attrs_for_style_string = _StyleStringToAttrsCache( style.get_attrs_for_style_str, style_transformation) for style_str, text, *_ in fragments: attrs = attrs_for_style_string[style_str] # Set style attributes if something changed. if attrs != last_attrs: if attrs: output.set_attributes(attrs, color_depth) else: output.reset_attributes() last_attrs = attrs # Eliminate carriage returns text = text.replace("\r", "") # Assume that the output is raw, and insert a carriage return before # every newline. (Also important when the front-end is a telnet client.) output.write(text.replace("\n", "\r\n")) # Reset again. output.reset_attributes() output.flush()