def render(self, cli, layout, style=None): """ Render the current interface to the output. """ style = style or Style output = self.output # When we render using another style, do a full repaint. (Forget about # the previous rendered screen.) if style != self._last_style: self._last_screen = None self._last_style = style # Enter alternate screen. if self.use_alternate_screen and not self._in_alternate_screen: self._in_alternate_screen = True output.enter_alternate_screen() # Create screen and write layout to it. size = output.get_size() screen = Screen(size.columns) if cli.is_done: height = 0 # When we are done, we don't necessary want to fill up until the bottom. else: height = self._last_screen.current_height if self._last_screen else 0 height = max(self._min_available_height, height) # When te size changes, don't consider the previous screen. if self._last_size != size: self._last_screen = None layout.write_to_screen( cli, screen, WritePosition( xpos=0, ypos=0, width=size.columns, height=(size.rows if self.use_alternate_screen else height), extended_height=size.rows, )) # When grayed. Replace all tokens in the new screen. if cli.is_aborting or cli.is_exiting: screen.replace_all_tokens(Token.Aborted) # Process diff and write to output. self._cursor_pos, self._last_char = output_screen_diff( output, screen, self._cursor_pos, self._last_screen, self._last_char, cli.is_done, style=style, ) self._last_screen = screen self._last_size = size output.flush()
def render(self, cli, layout, style=None, is_done=False): """ Render the current interface to the output. :param is_done: When True, put the cursor at the end of the interface. We won't print any changes to this part. """ style = style or Style output = self.output # When we render using another style, do a full repaint. (Forget about # the previous rendered screen.) if style != self._last_style: self._last_screen = None self._last_style = style # Enter alternate screen. if self.use_alternate_screen and not self._in_alternate_screen: self._in_alternate_screen = True output.enter_alternate_screen() # Create screen and write layout to it. size = output.get_size() screen = Screen(size.columns) if is_done: height = 0 # When we are done, we don't necessary want to fill up until the bottom. else: height = self._last_screen.current_height if self._last_screen else 0 height = max(self._min_available_height, height) # When te size changes, don't consider the previous screen. if self._last_size != size: self._last_screen = None layout.write_to_screen(cli, screen, WritePosition( xpos=0, ypos=0, width=size.columns, height=(size.rows if self.use_alternate_screen else height), extended_height=size.rows, )) # When grayed. Replace all tokens in the new screen. if cli.is_aborting or cli.is_exiting: screen.replace_all_tokens(Token.Aborted) # Process diff and write to output. self._cursor_pos, self._last_char = output_screen_diff( output, screen, self._cursor_pos, self._last_screen, self._last_char, is_done, style=style, ) self._last_screen = screen self._last_size = size output.flush()
def _reset_screen(self): """ Reset the Screen content. (also called when switching from/to alternate buffer. """ self.pt_screen = Screen(default_char=Char( ' ', '')) # TODO: maybe stop using this Screen class. self.pt_screen.cursor_position = CursorPosition(0, 0) self.pt_screen.show_cursor = True self.data_buffer = self.pt_screen.data_buffer self.pt_cursor_position = self.pt_screen.cursor_position self.wrapped_lines = [] # List of line indexes that were wrapped. self._attrs = Attrs(color=None, bgcolor=None, bold=False, underline=False, italic=False, blink=False, reverse=False, hidden=False) self._style_str = '' self.margins = None self.max_y = 0 # Max 'y' position to which is written.
def create_screen(self, cli, width, height): screen = Screen(initial_width=width) if self.pymux.arrangement.get_active_pane(cli) == self.arrangement_pane: token = Token.PaneNumber.Focussed else: token = Token.PaneNumber for i, d in enumerate('%s' % (self._get_index(cli))): _draw_number(screen, i * 6, int(d), token=token, default_token=Token.Transparent) return screen
def _reset_screen(self): """ Reset the Screen content. (also called when switching from/to alternate buffer. """ self.pt_screen = Screen(default_char=Char(' ', DEFAULT_TOKEN)) self.pt_screen.cursor_position = CursorPosition(0, 0) self.pt_screen.show_cursor = True self.data_buffer = self.pt_screen.data_buffer self.pt_cursor_position = self.pt_screen.cursor_position self._attrs = Attrs(color=None, bgcolor=None, bold=False, underline=False, italic=False, blink=False, reverse=False) self.margins = None self.max_y = 0 # Max 'y' position to which is written.
def _reset_screen(self): """(BetterScreen) -> NoneType Reset the Screen content. (also called when switching from/to alternate buffer. """ self.pt_screen = Screen(default_char=Char(' ', DEFAULT_TOKEN)) self.pt_screen.cursor_position = CursorPosition(0, 0) self.pt_screen.show_cursor = True self.data_buffer = self.pt_screen.data_buffer self._attrs = Attrs(color=None, bgcolor=None, bold=False, underline=False, italic=False, blink=False, reverse=False) self.margins = Margins(0, self.lines - 1) self.line_offset = 0 # Index of the line that's currently displayed on top. self.max_y = 0 # Max 'y' position to which is written.
def create_screen(self, cli, width, height): screen = Screen(initial_width=width) for y in range(self.HEIGHT): for x in range(self.WIDTH): screen.data_buffer[y][x] = Char(' ', Token) # Display time. now = datetime.datetime.now() _draw_number(screen, 0, now.hour // 10) _draw_number(screen, 6, now.hour % 10) _draw_number(screen, 16, now.minute // 10) _draw_number(screen, 23, now.minute % 10) # Add a colon screen.data_buffer[1][13] = Char(' ', Token.Clock) screen.data_buffer[3][13] = Char(' ', Token.Clock) screen.width = self.WIDTH screen.height = self.HEIGHT return screen
def render(self, app: 'Application[Any]', layout: 'Layout', is_done: bool = False) -> None: """ Render the current interface to the output. :param is_done: When True, put the cursor at the end of the interface. We won't print any changes to this part. """ output = self.output # Enter alternate screen. if self.full_screen and not self._in_alternate_screen: self._in_alternate_screen = True output.enter_alternate_screen() # Enable bracketed paste. if not self._bracketed_paste_enabled: self.output.enable_bracketed_paste() self._bracketed_paste_enabled = True # Enable/disable mouse support. needs_mouse_support = self.mouse_support() if needs_mouse_support and not self._mouse_support_enabled: output.enable_mouse_support() self._mouse_support_enabled = True elif not needs_mouse_support and self._mouse_support_enabled: output.disable_mouse_support() self._mouse_support_enabled = False # Create screen and write layout to it. size = output.get_size() screen = Screen() screen.show_cursor = False # Hide cursor by default, unless one of the # containers decides to display it. mouse_handlers = MouseHandlers() # Calculate height. if self.full_screen: height = size.rows elif is_done: # When we are done, we don't necessary want to fill up until the bottom. height = layout.container.preferred_height(size.columns, size.rows).preferred else: last_height = self._last_screen.height if self._last_screen else 0 height = max(self._min_available_height, last_height, layout.container.preferred_height(size.columns, size.rows).preferred) height = min(height, size.rows) # When te size changes, don't consider the previous screen. if self._last_size != size: self._last_screen = None # When we render using another style or another color depth, do a full # repaint. (Forget about the previous rendered screen.) # (But note that we still use _last_screen to calculate the height.) if (self.style.invalidation_hash() != self._last_style_hash or app.style_transformation.invalidation_hash() != self._last_transformation_hash or app.color_depth != self._last_color_depth): self._last_screen = None self._attrs_for_style = None if self._attrs_for_style is None: self._attrs_for_style = _StyleStringToAttrsCache( self.style.get_attrs_for_style_str, app.style_transformation) self._last_style_hash = self.style.invalidation_hash() self._last_transformation_hash = app.style_transformation.invalidation_hash() self._last_color_depth = app.color_depth layout.container.write_to_screen(screen, mouse_handlers, WritePosition( xpos=0, ypos=0, width=size.columns, height=height, ), parent_style='', erase_bg=False, z_index=None) screen.draw_all_floats() # When grayed. Replace all styles in the new screen. if app.exit_style: screen.append_style_to_content(app.exit_style) # Process diff and write to output. self._cursor_pos, self._last_style = _output_screen_diff( app, output, screen, self._cursor_pos, app.color_depth, self._last_screen, self._last_style, is_done, full_screen=self.full_screen, attrs_for_style_string=self._attrs_for_style, size=size, previous_width=(self._last_size.columns if self._last_size else 0)) self._last_screen = screen self._last_size = size self.mouse_handlers = mouse_handlers output.flush() # Set visible windows in layout. app.layout.visible_windows = screen.visible_windows if is_done: self.reset()
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 output_screen_diff(output, screen, current_pos, previous_screen=None, last_char=None, is_done=False, style=None): # XXX: drop is_done """ Create diff of this screen with the previous screen. This is some performance-critical code which is heavily optimized. Don't change things without profiling first. """ #: Remember the last printed character. last_char = [last_char] # nonlocal background_turned_on = [False] # Nonlocal #: Variable for capturing the output. write = output.write # 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 def reset_attributes(): " Wrapper around Output.reset_attributes. " _output_reset_attributes() last_char[0] = None # Forget last char after resetting attributes. def move_cursor(new): " 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 meight 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 >= screen.width - 1: write('\r') _output_cursor_forward(new.x) elif new.x < current_x or current_x >= screen.width - 1: _output_cursor_backward(current_x - new.x) elif new.x > current_x: _output_cursor_forward(new.x - current_x) return new style_for_token = _StyleForTokenCache(style) def output_char(char): """ Write the output of this character. """ # If the last printed character has the same token, it also has the # same style, so we don't output it. if last_char[0] and last_char[0].token == char.token: write(char.char) else: style = style_for_token[char.token] if style: _output_set_attributes(style['color'], style['bgcolor'], bold=style.get('bold', False), underline=style.get('underline', False)) # If we print something with a background color, remember that. background_turned_on[0] = bool(style['bgcolor']) else: # Reset previous style and output. reset_attributes() write(char.char) last_char[0] = char # Disable autowrap if not previous_screen: output.disable_autowrap() reset_attributes() # When the previous screen has a different size, redraw everything anyway. # Also when we are done. (We meight take up less rows, so clearing is important.) if is_done or not previous_screen or previous_screen.width != screen.width: # XXX: also consider height?? current_pos = move_cursor(Point(0, 0)) reset_attributes() output.erase_down() previous_screen = Screen(screen.width) # Get height of the screen. # (current_height changes as we loop over _buffer, so remember the current value.) current_height = screen.current_height # Loop over the rows. row_count = max(screen.current_height, previous_screen.current_height) c = 0 # Column counter. for y, r in enumerate(range(0, row_count)): new_row = screen._buffer[r] previous_row = previous_screen._buffer[r] new_max_line_len = max(new_row.keys()) if new_row else 0 previous_max_line_len = 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.token != old_char.token: current_pos = move_cursor(Point(y=y, x=c)) output_char(new_char) current_pos = current_pos._replace(x=current_pos.x + char_width) 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(y=y, x=new_max_line_len + 1)) 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 screen.current_height > previous_screen.current_height: current_pos = move_cursor(Point(y=screen.current_height - 1, x=0)) # Move cursor: if is_done: current_pos = move_cursor(Point(y=current_height, x=0)) output.erase_down() else: current_pos = move_cursor(screen.cursor_position) if is_done: reset_attributes() output.enable_autowrap() # If the last printed character has a background color, always reset. # (Many terminals give weird artifacs on resize events when there is an # active background color.) if background_turned_on[0]: reset_attributes() return current_pos, last_char[0]
def render(self, cli, layout, style=None, is_done=False): """ Render the current interface to the output. :param is_done: When True, put the cursor at the end of the interface. We won't print any changes to this part. """ style = style or Style output = self.output # Enter alternate screen. if self.use_alternate_screen and not self._in_alternate_screen: self._in_alternate_screen = True output.enter_alternate_screen() # Create screen and write layout to it. size = output.get_size() screen = Screen(size.columns) if is_done: height = 0 # When we are done, we don't necessary want to fill up until the bottom. else: height = self._last_screen.current_height if self._last_screen else 0 height = max(self._min_available_height, height) # When te size changes, don't consider the previous screen. if self._last_size != size: self._last_screen = None # When we render using another style, do a full repaint. (Forget about # the previous rendered screen.) # (But note that we still use _last_screen to calculate the height.) if style != self._last_style: self._last_screen = None self._last_style = style layout.write_to_screen( cli, screen, WritePosition( xpos=0, ypos=0, width=size.columns, height=(size.rows if self.use_alternate_screen else height), extended_height=size.rows, )) # When grayed. Replace all tokens in the new screen. if cli.is_aborting or cli.is_exiting: screen.replace_all_tokens(Token.Aborted) # Process diff and write to output. self._cursor_pos, self._last_char = output_screen_diff( output, screen, self._cursor_pos, self._last_screen, self._last_char, is_done, style=style, ) self._last_screen = screen self._last_size = size # Write title if it changed. new_title = cli.terminal_title if new_title != self._last_title: if new_title is None: self.output.clear_title() else: self.output.set_title(new_title) self._last_title = new_title output.flush()
def render(self, cli, layout, is_done=False): """ Render the current interface to the output. :param is_done: When True, put the cursor at the end of the interface. We won't print any changes to this part. """ output = self.output # Enter alternate screen. if self.use_alternate_screen and not self._in_alternate_screen: self._in_alternate_screen = True output.enter_alternate_screen() # Enable bracketed paste. if not self._bracketed_paste_enabled: self.output.enable_bracketed_paste() self._bracketed_paste_enabled = True # Enable/disable mouse support. needs_mouse_support = self.mouse_support(cli) if needs_mouse_support and not self._mouse_support_enabled: output.enable_mouse_support() self._mouse_support_enabled = True elif not needs_mouse_support and self._mouse_support_enabled: output.disable_mouse_support() self._mouse_support_enabled = False # Create screen and write layout to it. size = output.get_size() screen = Screen() screen.show_cursor = False # Hide cursor by default, unless one of the # containers decides to display it. mouse_handlers = MouseHandlers() if is_done: height = 0 # When we are done, we don't necessary want to fill up until the bottom. else: height = self._last_screen.height if self._last_screen else 0 height = max(self._min_available_height, height) # When te size changes, don't consider the previous screen. if self._last_size != size: self._last_screen = None # When we render using another style, do a full repaint. (Forget about # the previous rendered screen.) # (But note that we still use _last_screen to calculate the height.) if self.style.invalidation_hash() != self._last_style_hash: self._last_screen = None self._attrs_for_token = None if self._attrs_for_token is None: self._attrs_for_token = _TokenToAttrsCache( self.style.get_attrs_for_token) self._last_style_hash = self.style.invalidation_hash() layout.write_to_screen( cli, screen, mouse_handlers, WritePosition( xpos=0, ypos=0, width=size.columns, height=(size.rows if self.use_alternate_screen else height), extended_height=size.rows, )) # When grayed. Replace all tokens in the new screen. if cli.is_aborting or cli.is_exiting: screen.replace_all_tokens(Token.Aborted) # Process diff and write to output. self._cursor_pos, self._last_char = output_screen_diff( output, screen, self._cursor_pos, self._last_screen, self._last_char, is_done, attrs_for_token=self._attrs_for_token, width=size.columns, previous_width=(self._last_size.columns if self._last_size else 0)) self._last_screen = screen self._last_size = size self.mouse_handlers = mouse_handlers # Write title if it changed. new_title = cli.terminal_title if new_title != self._last_title: if new_title is None: self.output.clear_title() else: self.output.set_title(new_title) self._last_title = new_title output.flush()
def setUp(self): self.screen = Screen(Size(rows=10, columns=80))
def _output_screen_diff(output, screen, current_pos, previous_screen=None, last_char=None, is_done=False, attrs_for_token=None, size=None, previous_width=0): # XXX: drop is_done """ 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_char: `Char` instance that represents the output attributes of the last drawn character. (Color/attributes.) :param attrs_for_token: :class:`._TokenToAttrsCache` instance. :param width: The width of the terminal. :param prevous_width: The width of the terminal during the last rendering. """ width, height = size.columns, size.rows #: Remember the last printed character. last_char = [last_char] # nonlocal background_turned_on = [False] # Nonlocal #: 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(): " Wrapper around Output.reset_attributes. " _output_reset_attributes() last_char[0] = None # Forget last char after resetting attributes. def move_cursor(new): " 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 meight 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): """ Write the output of this character. """ # If the last printed character has the same token, it also has the # same style, so we don't output it. if last_char[0] and last_char[0].token == char.token: write(char.char) else: attrs = attrs_for_token[char.token] _output_set_attributes(attrs) # If we print something with a background color, remember that. background_turned_on[0] = bool(attrs.bgcolor) write(char.char) last_char[0] = char # Disable autowrap if not previous_screen: output.disable_autowrap() reset_attributes() # When the previous screen has a different size, redraw everything anyway. # Also when we are done. (We meight 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(0, 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.token != old_char.token: current_pos = move_cursor(Point(y=y, x=c)) # 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 = current_pos._replace(x=current_pos.x + char_width) 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(y=y, x=new_max_line_len + 1)) 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(y=current_height - 1, x=0)) # Move cursor: if is_done: current_pos = move_cursor(Point(y=current_height, x=0)) output.erase_down() else: current_pos = move_cursor(screen.cursor_position) if is_done: reset_attributes() output.enable_autowrap() # If the last printed character has a background color, always reset. # (Many terminals give weird artifacs on resize events when there is an # active background color.) if background_turned_on[0]: reset_attributes() if screen.show_cursor or is_done: output.show_cursor() return current_pos, last_char[0]
def output_screen_diff(output, screen, current_pos, previous_screen=None, last_char=None, is_done=False, style=None): # XXX: drop is_done """ Create diff of this screen with the previous screen. """ #: Remember the last printed character. last_char = [last_char] # nonlocal background_turned_on = [False] # Nonlocal #: Variable for capturing the output. write = output.write def move_cursor(new): current_x, current_y = current_pos.x, current_pos.y if new.y > current_y: # Use newlines instead of CURSOR_DOWN, because this meight add new lines. # CURSOR_DOWN will never create new lines at the bottom. # Also reset attributes, otherwise the newline could draw a # background color. output.reset_attributes() write('\r\n' * (new.y - current_y)) current_x = 0 output.cursor_forward(new.x) last_char[0] = None # Forget last char after resetting attributes. return new elif new.y < current_y: output.cursor_up(current_y - new.y) if current_x >= screen.width - 1: write('\r') output.cursor_forward(new.x) elif new.x < current_x or current_x >= screen.width - 1: output.cursor_backward(current_x - new.x) elif new.x > current_x: output.cursor_forward(new.x - current_x) return new style_for_token = _StyleForTokenCache(style) def output_char(char): """ Write the output of this character. """ # If the last printed character has the same token, it also has the # same style, so we don't output it. if last_char[0] and last_char[0].token == char.token: write(char.char) else: style = style_for_token[char.token] if style: output.set_attributes(style['color'], style['bgcolor'], bold=style.get('bold', False), underline=style.get('underline', False)) # If we print something with a background color, remember that. background_turned_on[0] = bool(style['bgcolor']) else: # Reset previous style and output. output.reset_attributes() write(char.char) last_char[0] = char # Disable autowrap if not previous_screen: output.disable_autowrap() output.reset_attributes() # When the previous screen has a different size, redraw everything anyway. # Also when we are done. (We meight take up less rows, so clearing is important.) if is_done or not previous_screen or previous_screen.width != screen.width: # XXX: also consider height?? current_pos = move_cursor(Point(0, 0)) output.reset_attributes() output.erase_down() previous_screen = Screen(screen.width) # Get height of the screen. # (current_height changes as we loop over _buffer, so remember the current value.) current_height = screen.current_height # Loop over the rows. row_count = max(screen.current_height, previous_screen.current_height) c = 0 # Column counter. for y, r in enumerate(range(0, row_count)): new_row = screen._buffer[r] previous_row = previous_screen._buffer[r] new_max_line_len = max(new_row.keys()) if new_row else 0 previous_max_line_len = 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.token != old_char.token: current_pos = move_cursor(Point(y=y, x=c)) output_char(new_char) current_pos = current_pos._replace(x=current_pos.x + char_width) 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(y=y, x=new_max_line_len + 1)) output.reset_attributes() output.erase_end_of_line() last_char[0] = None # Forget last char after resetting attributes. # Move cursor: if is_done: current_pos = move_cursor(Point(y=current_height, x=0)) output.erase_down() else: current_pos = move_cursor(screen.cursor_position) if is_done: output.reset_attributes() output.enable_autowrap() # If the last printed character has a background color, always reset. # (Many terminals give weird artifacs on resize events when there is an # active background color.) if background_turned_on[0]: output.reset_attributes() last_char[0] = None return current_pos, last_char[0]
def render(self, cli, layout, is_done=False): """ Render the current interface to the output. :param is_done: When True, put the cursor at the end of the interface. We won't print any changes to this part. """ output = self.output # Enter alternate screen. if self.use_alternate_screen and not self._in_alternate_screen: self._in_alternate_screen = True output.enter_alternate_screen() # Enable bracketed paste. if not self._bracketed_paste_enabled: self.output.enable_bracketed_paste() self._bracketed_paste_enabled = True # Enable/disable mouse support. needs_mouse_support = self.mouse_support(cli) if needs_mouse_support and not self._mouse_support_enabled: output.enable_mouse_support() self._mouse_support_enabled = True elif not needs_mouse_support and self._mouse_support_enabled: output.disable_mouse_support() self._mouse_support_enabled = False # Create screen and write layout to it. size = output.get_size() screen = Screen() screen.show_cursor = False # Hide cursor by default, unless one of the # containers decides to display it. mouse_handlers = MouseHandlers() if is_done: height = 0 # When we are done, we don't necessary want to fill up until the bottom. else: height = self._last_screen.height if self._last_screen else 0 height = max(self._min_available_height, height) # When te size changes, don't consider the previous screen. if self._last_size != size: self._last_screen = None # When we render using another style, do a full repaint. (Forget about # the previous rendered screen.) # (But note that we still use _last_screen to calculate the height.) if self.style.invalidation_hash() != self._last_style_hash: self._last_screen = None self._attrs_for_token = None if self._attrs_for_token is None: self._attrs_for_token = _TokenToAttrsCache(self.style.get_attrs_for_token) self._last_style_hash = self.style.invalidation_hash() layout.write_to_screen(cli, screen, mouse_handlers, WritePosition( xpos=0, ypos=0, width=size.columns, height=(size.rows if self.use_alternate_screen else height), extended_height=size.rows, )) # When grayed. Replace all tokens in the new screen. if cli.is_aborting or cli.is_exiting: screen.replace_all_tokens(Token.Aborted) # Process diff and write to output. self._cursor_pos, self._last_char = _output_screen_diff( output, screen, self._cursor_pos, self._last_screen, self._last_char, is_done, attrs_for_token=self._attrs_for_token, size=size, previous_width=(self._last_size.columns if self._last_size else 0)) self._last_screen = screen self._last_size = size self.mouse_handlers = mouse_handlers # Write title if it changed. new_title = cli.terminal_title if new_title != self._last_title: if new_title is None: self.output.clear_title() else: self.output.set_title(new_title) self._last_title = new_title output.flush()
def _reflow(self): """ Reflow the screen using the given width. """ width = self.columns data_buffer = self.pt_screen.data_buffer new_data_buffer = Screen(default_char=Char(' ', '')).data_buffer cursor_position = self.pt_screen.cursor_position cy, cx = (cursor_position.y, cursor_position.x) cursor_character = data_buffer[cursor_position.y][ cursor_position.x].char # Ensure that the cursor position is present. # (and avoid calling min() on empty collection.) data_buffer[cursor_position.y][cursor_position.y] # Unwrap all the lines. offset = min(data_buffer) line = [] all_lines = [line] for row_index in range(min(data_buffer), max(data_buffer) + 1): row = data_buffer[row_index] row[0] # Avoid calling max() on empty collection. for column_index in range(0, max(row) + 1): if cy == row_index and cx == column_index: cy = len(all_lines) - 1 cx = len(line) line.append(row[column_index]) # Create new line if the next line was not a wrapped line. if row_index + 1 not in self.wrapped_lines: line = [] all_lines.append(line) # Remove trailing whitespace (unless it contains the cursor). # Also make sure that lines consist of at lesat one character, # otherwise we can't calculate `max_y` correctly. (This is important # for the `clear` command.) for row_index, line in enumerate(all_lines): # We do this only if no special styling given. while len(line) > 1 and line[-1].char.isspace( ) and not line[-1].style: if row_index == cy and len(line) - 1 == cx: break line.pop() # Wrap lines again according to the screen width. new_row_index = offset new_column_index = 0 new_wrapped_lines = [] for row_index, line in enumerate(all_lines): for column_index, char in enumerate(line): # Check for space on the current line. if new_column_index + char.width > width: new_row_index += 1 new_column_index = 0 new_wrapped_lines.append(new_row_index) if cy == row_index and cx == column_index: cy = new_row_index cx = new_column_index # Add character to new buffer. new_data_buffer[new_row_index][new_column_index] = char new_column_index += char.width new_row_index += 1 new_column_index = 0 # TODO: when the window gets smaller, and the cursor is at the top of the screen, # remove lines at the bottom. for row_index in range(min(data_buffer), max(data_buffer) + 1): if row_index > cy + self.lines: del data_buffer[row_index] self.pt_screen.data_buffer = new_data_buffer self.data_buffer = new_data_buffer self.wrapped_lines = new_wrapped_lines cursor_position.y, cursor_position.x = cy, cx self.pt_screen.cursor_position = cursor_position # XXX: not needed. self.pt_cursor_position = self.pt_screen.cursor_position # If everything goes well, the cursor should still be on the same character. if cursor_character != new_data_buffer[cursor_position.y][ cursor_position.x].char: # FIXME: raise Exception( 'Reflow failed: %r %r' % (cursor_character, new_data_buffer[cursor_position.y][cursor_position.x].char)) self.max_y = max(self.data_buffer) self.max_y = min(self.max_y, cursor_position.y + self.lines - 1)