Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/rich/console.py: 47%
947 statements
« prev ^ index » next coverage.py v7.0.1, created at 2022-12-25 06:11 +0000
« prev ^ index » next coverage.py v7.0.1, created at 2022-12-25 06:11 +0000
1import inspect
2import io
3import os
4import platform
5import sys
6import threading
7import zlib
8from abc import ABC, abstractmethod
9from dataclasses import dataclass, field
10from datetime import datetime
11from functools import wraps
12from getpass import getpass
13from html import escape
14from inspect import isclass
15from itertools import islice
16from math import ceil
17from time import monotonic
18from types import FrameType, ModuleType, TracebackType
19from typing import (
20 IO,
21 TYPE_CHECKING,
22 Any,
23 Callable,
24 Dict,
25 Iterable,
26 List,
27 Mapping,
28 NamedTuple,
29 Optional,
30 TextIO,
31 Tuple,
32 Type,
33 Union,
34 cast,
35)
37from rich._null_file import NULL_FILE
39if sys.version_info >= (3, 8):
40 from typing import Literal, Protocol, runtime_checkable
41else:
42 from typing_extensions import (
43 Literal,
44 Protocol,
45 runtime_checkable,
46 ) # pragma: no cover
48from . import errors, themes
49from ._emoji_replace import _emoji_replace
50from ._export_format import CONSOLE_HTML_FORMAT, CONSOLE_SVG_FORMAT
51from ._log_render import FormatTimeCallable, LogRender
52from .align import Align, AlignMethod
53from .color import ColorSystem, blend_rgb
54from .control import Control
55from .emoji import EmojiVariant
56from .highlighter import NullHighlighter, ReprHighlighter
57from .markup import render as render_markup
58from .measure import Measurement, measure_renderables
59from .pager import Pager, SystemPager
60from .pretty import Pretty, is_expandable
61from .protocol import rich_cast
62from .region import Region
63from .scope import render_scope
64from .screen import Screen
65from .segment import Segment
66from .style import Style, StyleType
67from .styled import Styled
68from .terminal_theme import DEFAULT_TERMINAL_THEME, SVG_EXPORT_THEME, TerminalTheme
69from .text import Text, TextType
70from .theme import Theme, ThemeStack
72if TYPE_CHECKING:
73 from ._windows import WindowsConsoleFeatures
74 from .live import Live
75 from .status import Status
77JUPYTER_DEFAULT_COLUMNS = 115
78JUPYTER_DEFAULT_LINES = 100
79WINDOWS = platform.system() == "Windows"
81HighlighterType = Callable[[Union[str, "Text"]], "Text"]
82JustifyMethod = Literal["default", "left", "center", "right", "full"]
83OverflowMethod = Literal["fold", "crop", "ellipsis", "ignore"]
86class NoChange:
87 pass
90NO_CHANGE = NoChange()
92try:
93 _STDIN_FILENO = sys.__stdin__.fileno()
94except Exception:
95 _STDIN_FILENO = 0
96try:
97 _STDOUT_FILENO = sys.__stdout__.fileno()
98except Exception:
99 _STDOUT_FILENO = 1
100try:
101 _STDERR_FILENO = sys.__stderr__.fileno()
102except Exception:
103 _STDERR_FILENO = 2
105_STD_STREAMS = (_STDIN_FILENO, _STDOUT_FILENO, _STDERR_FILENO)
106_STD_STREAMS_OUTPUT = (_STDOUT_FILENO, _STDERR_FILENO)
109_TERM_COLORS = {
110 "kitty": ColorSystem.EIGHT_BIT,
111 "256color": ColorSystem.EIGHT_BIT,
112 "16color": ColorSystem.STANDARD,
113}
116class ConsoleDimensions(NamedTuple):
117 """Size of the terminal."""
119 width: int
120 """The width of the console in 'cells'."""
121 height: int
122 """The height of the console in lines."""
125@dataclass
126class ConsoleOptions:
127 """Options for __rich_console__ method."""
129 size: ConsoleDimensions
130 """Size of console."""
131 legacy_windows: bool
132 """legacy_windows: flag for legacy windows."""
133 min_width: int
134 """Minimum width of renderable."""
135 max_width: int
136 """Maximum width of renderable."""
137 is_terminal: bool
138 """True if the target is a terminal, otherwise False."""
139 encoding: str
140 """Encoding of terminal."""
141 max_height: int
142 """Height of container (starts as terminal)"""
143 justify: Optional[JustifyMethod] = None
144 """Justify value override for renderable."""
145 overflow: Optional[OverflowMethod] = None
146 """Overflow value override for renderable."""
147 no_wrap: Optional[bool] = False
148 """Disable wrapping for text."""
149 highlight: Optional[bool] = None
150 """Highlight override for render_str."""
151 markup: Optional[bool] = None
152 """Enable markup when rendering strings."""
153 height: Optional[int] = None
155 @property
156 def ascii_only(self) -> bool:
157 """Check if renderables should use ascii only."""
158 return not self.encoding.startswith("utf")
160 def copy(self) -> "ConsoleOptions":
161 """Return a copy of the options.
163 Returns:
164 ConsoleOptions: a copy of self.
165 """
166 options: ConsoleOptions = ConsoleOptions.__new__(ConsoleOptions)
167 options.__dict__ = self.__dict__.copy()
168 return options
170 def update(
171 self,
172 *,
173 width: Union[int, NoChange] = NO_CHANGE,
174 min_width: Union[int, NoChange] = NO_CHANGE,
175 max_width: Union[int, NoChange] = NO_CHANGE,
176 justify: Union[Optional[JustifyMethod], NoChange] = NO_CHANGE,
177 overflow: Union[Optional[OverflowMethod], NoChange] = NO_CHANGE,
178 no_wrap: Union[Optional[bool], NoChange] = NO_CHANGE,
179 highlight: Union[Optional[bool], NoChange] = NO_CHANGE,
180 markup: Union[Optional[bool], NoChange] = NO_CHANGE,
181 height: Union[Optional[int], NoChange] = NO_CHANGE,
182 ) -> "ConsoleOptions":
183 """Update values, return a copy."""
184 options = self.copy()
185 if not isinstance(width, NoChange):
186 options.min_width = options.max_width = max(0, width)
187 if not isinstance(min_width, NoChange):
188 options.min_width = min_width
189 if not isinstance(max_width, NoChange):
190 options.max_width = max_width
191 if not isinstance(justify, NoChange):
192 options.justify = justify
193 if not isinstance(overflow, NoChange):
194 options.overflow = overflow
195 if not isinstance(no_wrap, NoChange):
196 options.no_wrap = no_wrap
197 if not isinstance(highlight, NoChange):
198 options.highlight = highlight
199 if not isinstance(markup, NoChange):
200 options.markup = markup
201 if not isinstance(height, NoChange):
202 if height is not None:
203 options.max_height = height
204 options.height = None if height is None else max(0, height)
205 return options
207 def update_width(self, width: int) -> "ConsoleOptions":
208 """Update just the width, return a copy.
210 Args:
211 width (int): New width (sets both min_width and max_width)
213 Returns:
214 ~ConsoleOptions: New console options instance.
215 """
216 options = self.copy()
217 options.min_width = options.max_width = max(0, width)
218 return options
220 def update_height(self, height: int) -> "ConsoleOptions":
221 """Update the height, and return a copy.
223 Args:
224 height (int): New height
226 Returns:
227 ~ConsoleOptions: New Console options instance.
228 """
229 options = self.copy()
230 options.max_height = options.height = height
231 return options
233 def reset_height(self) -> "ConsoleOptions":
234 """Return a copy of the options with height set to ``None``.
236 Returns:
237 ~ConsoleOptions: New console options instance.
238 """
239 options = self.copy()
240 options.height = None
241 return options
243 def update_dimensions(self, width: int, height: int) -> "ConsoleOptions":
244 """Update the width and height, and return a copy.
246 Args:
247 width (int): New width (sets both min_width and max_width).
248 height (int): New height.
250 Returns:
251 ~ConsoleOptions: New console options instance.
252 """
253 options = self.copy()
254 options.min_width = options.max_width = max(0, width)
255 options.height = options.max_height = height
256 return options
259@runtime_checkable
260class RichCast(Protocol):
261 """An object that may be 'cast' to a console renderable."""
263 def __rich__(
264 self,
265 ) -> Union["ConsoleRenderable", "RichCast", str]: # pragma: no cover
266 ...
269@runtime_checkable
270class ConsoleRenderable(Protocol):
271 """An object that supports the console protocol."""
273 def __rich_console__(
274 self, console: "Console", options: "ConsoleOptions"
275 ) -> "RenderResult": # pragma: no cover
276 ...
279# A type that may be rendered by Console.
280RenderableType = Union[ConsoleRenderable, RichCast, str]
282# The result of calling a __rich_console__ method.
283RenderResult = Iterable[Union[RenderableType, Segment]]
285_null_highlighter = NullHighlighter()
288class CaptureError(Exception):
289 """An error in the Capture context manager."""
292class NewLine:
293 """A renderable to generate new line(s)"""
295 def __init__(self, count: int = 1) -> None:
296 self.count = count
298 def __rich_console__(
299 self, console: "Console", options: "ConsoleOptions"
300 ) -> Iterable[Segment]:
301 yield Segment("\n" * self.count)
304class ScreenUpdate:
305 """Render a list of lines at a given offset."""
307 def __init__(self, lines: List[List[Segment]], x: int, y: int) -> None:
308 self._lines = lines
309 self.x = x
310 self.y = y
312 def __rich_console__(
313 self, console: "Console", options: ConsoleOptions
314 ) -> RenderResult:
315 x = self.x
316 move_to = Control.move_to
317 for offset, line in enumerate(self._lines, self.y):
318 yield move_to(x, offset)
319 yield from line
322class Capture:
323 """Context manager to capture the result of printing to the console.
324 See :meth:`~rich.console.Console.capture` for how to use.
326 Args:
327 console (Console): A console instance to capture output.
328 """
330 def __init__(self, console: "Console") -> None:
331 self._console = console
332 self._result: Optional[str] = None
334 def __enter__(self) -> "Capture":
335 self._console.begin_capture()
336 return self
338 def __exit__(
339 self,
340 exc_type: Optional[Type[BaseException]],
341 exc_val: Optional[BaseException],
342 exc_tb: Optional[TracebackType],
343 ) -> None:
344 self._result = self._console.end_capture()
346 def get(self) -> str:
347 """Get the result of the capture."""
348 if self._result is None:
349 raise CaptureError(
350 "Capture result is not available until context manager exits."
351 )
352 return self._result
355class ThemeContext:
356 """A context manager to use a temporary theme. See :meth:`~rich.console.Console.use_theme` for usage."""
358 def __init__(self, console: "Console", theme: Theme, inherit: bool = True) -> None:
359 self.console = console
360 self.theme = theme
361 self.inherit = inherit
363 def __enter__(self) -> "ThemeContext":
364 self.console.push_theme(self.theme)
365 return self
367 def __exit__(
368 self,
369 exc_type: Optional[Type[BaseException]],
370 exc_val: Optional[BaseException],
371 exc_tb: Optional[TracebackType],
372 ) -> None:
373 self.console.pop_theme()
376class PagerContext:
377 """A context manager that 'pages' content. See :meth:`~rich.console.Console.pager` for usage."""
379 def __init__(
380 self,
381 console: "Console",
382 pager: Optional[Pager] = None,
383 styles: bool = False,
384 links: bool = False,
385 ) -> None:
386 self._console = console
387 self.pager = SystemPager() if pager is None else pager
388 self.styles = styles
389 self.links = links
391 def __enter__(self) -> "PagerContext":
392 self._console._enter_buffer()
393 return self
395 def __exit__(
396 self,
397 exc_type: Optional[Type[BaseException]],
398 exc_val: Optional[BaseException],
399 exc_tb: Optional[TracebackType],
400 ) -> None:
401 if exc_type is None:
402 with self._console._lock:
403 buffer: List[Segment] = self._console._buffer[:]
404 del self._console._buffer[:]
405 segments: Iterable[Segment] = buffer
406 if not self.styles:
407 segments = Segment.strip_styles(segments)
408 elif not self.links:
409 segments = Segment.strip_links(segments)
410 content = self._console._render_buffer(segments)
411 self.pager.show(content)
412 self._console._exit_buffer()
415class ScreenContext:
416 """A context manager that enables an alternative screen. See :meth:`~rich.console.Console.screen` for usage."""
418 def __init__(
419 self, console: "Console", hide_cursor: bool, style: StyleType = ""
420 ) -> None:
421 self.console = console
422 self.hide_cursor = hide_cursor
423 self.screen = Screen(style=style)
424 self._changed = False
426 def update(
427 self, *renderables: RenderableType, style: Optional[StyleType] = None
428 ) -> None:
429 """Update the screen.
431 Args:
432 renderable (RenderableType, optional): Optional renderable to replace current renderable,
433 or None for no change. Defaults to None.
434 style: (Style, optional): Replacement style, or None for no change. Defaults to None.
435 """
436 if renderables:
437 self.screen.renderable = (
438 Group(*renderables) if len(renderables) > 1 else renderables[0]
439 )
440 if style is not None:
441 self.screen.style = style
442 self.console.print(self.screen, end="")
444 def __enter__(self) -> "ScreenContext":
445 self._changed = self.console.set_alt_screen(True)
446 if self._changed and self.hide_cursor:
447 self.console.show_cursor(False)
448 return self
450 def __exit__(
451 self,
452 exc_type: Optional[Type[BaseException]],
453 exc_val: Optional[BaseException],
454 exc_tb: Optional[TracebackType],
455 ) -> None:
456 if self._changed:
457 self.console.set_alt_screen(False)
458 if self.hide_cursor:
459 self.console.show_cursor(True)
462class Group:
463 """Takes a group of renderables and returns a renderable object that renders the group.
465 Args:
466 renderables (Iterable[RenderableType]): An iterable of renderable objects.
467 fit (bool, optional): Fit dimension of group to contents, or fill available space. Defaults to True.
468 """
470 def __init__(self, *renderables: "RenderableType", fit: bool = True) -> None:
471 self._renderables = renderables
472 self.fit = fit
473 self._render: Optional[List[RenderableType]] = None
475 @property
476 def renderables(self) -> List["RenderableType"]:
477 if self._render is None:
478 self._render = list(self._renderables)
479 return self._render
481 def __rich_measure__(
482 self, console: "Console", options: "ConsoleOptions"
483 ) -> "Measurement":
484 if self.fit:
485 return measure_renderables(console, options, self.renderables)
486 else:
487 return Measurement(options.max_width, options.max_width)
489 def __rich_console__(
490 self, console: "Console", options: "ConsoleOptions"
491 ) -> RenderResult:
492 yield from self.renderables
495def group(fit: bool = True) -> Callable[..., Callable[..., Group]]:
496 """A decorator that turns an iterable of renderables in to a group.
498 Args:
499 fit (bool, optional): Fit dimension of group to contents, or fill available space. Defaults to True.
500 """
502 def decorator(
503 method: Callable[..., Iterable[RenderableType]]
504 ) -> Callable[..., Group]:
505 """Convert a method that returns an iterable of renderables in to a Group."""
507 @wraps(method)
508 def _replace(*args: Any, **kwargs: Any) -> Group:
509 renderables = method(*args, **kwargs)
510 return Group(*renderables, fit=fit)
512 return _replace
514 return decorator
517def _is_jupyter() -> bool: # pragma: no cover
518 """Check if we're running in a Jupyter notebook."""
519 try:
520 get_ipython # type: ignore[name-defined]
521 except NameError:
522 return False
523 ipython = get_ipython() # type: ignore[name-defined]
524 shell = ipython.__class__.__name__
525 if (
526 "google.colab" in str(ipython.__class__)
527 or os.getenv("DATABRICKS_RUNTIME_VERSION")
528 or shell == "ZMQInteractiveShell"
529 ):
530 return True # Jupyter notebook or qtconsole
531 elif shell == "TerminalInteractiveShell":
532 return False # Terminal running IPython
533 else:
534 return False # Other type (?)
537COLOR_SYSTEMS = {
538 "standard": ColorSystem.STANDARD,
539 "256": ColorSystem.EIGHT_BIT,
540 "truecolor": ColorSystem.TRUECOLOR,
541 "windows": ColorSystem.WINDOWS,
542}
544_COLOR_SYSTEMS_NAMES = {system: name for name, system in COLOR_SYSTEMS.items()}
547@dataclass
548class ConsoleThreadLocals(threading.local):
549 """Thread local values for Console context."""
551 theme_stack: ThemeStack
552 buffer: List[Segment] = field(default_factory=list)
553 buffer_index: int = 0
556class RenderHook(ABC):
557 """Provides hooks in to the render process."""
559 @abstractmethod
560 def process_renderables(
561 self, renderables: List[ConsoleRenderable]
562 ) -> List[ConsoleRenderable]:
563 """Called with a list of objects to render.
565 This method can return a new list of renderables, or modify and return the same list.
567 Args:
568 renderables (List[ConsoleRenderable]): A number of renderable objects.
570 Returns:
571 List[ConsoleRenderable]: A replacement list of renderables.
572 """
575_windows_console_features: Optional["WindowsConsoleFeatures"] = None
578def get_windows_console_features() -> "WindowsConsoleFeatures": # pragma: no cover
579 global _windows_console_features
580 if _windows_console_features is not None:
581 return _windows_console_features
582 from ._windows import get_windows_console_features
584 _windows_console_features = get_windows_console_features()
585 return _windows_console_features
588def detect_legacy_windows() -> bool:
589 """Detect legacy Windows."""
590 return WINDOWS and not get_windows_console_features().vt
593class Console:
594 """A high level console interface.
596 Args:
597 color_system (str, optional): The color system supported by your terminal,
598 either ``"standard"``, ``"256"`` or ``"truecolor"``. Leave as ``"auto"`` to autodetect.
599 force_terminal (Optional[bool], optional): Enable/disable terminal control codes, or None to auto-detect terminal. Defaults to None.
600 force_jupyter (Optional[bool], optional): Enable/disable Jupyter rendering, or None to auto-detect Jupyter. Defaults to None.
601 force_interactive (Optional[bool], optional): Enable/disable interactive mode, or None to auto detect. Defaults to None.
602 soft_wrap (Optional[bool], optional): Set soft wrap default on print method. Defaults to False.
603 theme (Theme, optional): An optional style theme object, or ``None`` for default theme.
604 stderr (bool, optional): Use stderr rather than stdout if ``file`` is not specified. Defaults to False.
605 file (IO, optional): A file object where the console should write to. Defaults to stdout.
606 quiet (bool, Optional): Boolean to suppress all output. Defaults to False.
607 width (int, optional): The width of the terminal. Leave as default to auto-detect width.
608 height (int, optional): The height of the terminal. Leave as default to auto-detect height.
609 style (StyleType, optional): Style to apply to all output, or None for no style. Defaults to None.
610 no_color (Optional[bool], optional): Enabled no color mode, or None to auto detect. Defaults to None.
611 tab_size (int, optional): Number of spaces used to replace a tab character. Defaults to 8.
612 record (bool, optional): Boolean to enable recording of terminal output,
613 required to call :meth:`export_html`, :meth:`export_svg`, and :meth:`export_text`. Defaults to False.
614 markup (bool, optional): Boolean to enable :ref:`console_markup`. Defaults to True.
615 emoji (bool, optional): Enable emoji code. Defaults to True.
616 emoji_variant (str, optional): Optional emoji variant, either "text" or "emoji". Defaults to None.
617 highlight (bool, optional): Enable automatic highlighting. Defaults to True.
618 log_time (bool, optional): Boolean to enable logging of time by :meth:`log` methods. Defaults to True.
619 log_path (bool, optional): Boolean to enable the logging of the caller by :meth:`log`. Defaults to True.
620 log_time_format (Union[str, TimeFormatterCallable], optional): If ``log_time`` is enabled, either string for strftime or callable that formats the time. Defaults to "[%X] ".
621 highlighter (HighlighterType, optional): Default highlighter.
622 legacy_windows (bool, optional): Enable legacy Windows mode, or ``None`` to auto detect. Defaults to ``None``.
623 safe_box (bool, optional): Restrict box options that don't render on legacy Windows.
624 get_datetime (Callable[[], datetime], optional): Callable that gets the current time as a datetime.datetime object (used by Console.log),
625 or None for datetime.now.
626 get_time (Callable[[], time], optional): Callable that gets the current time in seconds, default uses time.monotonic.
627 """
629 _environ: Mapping[str, str] = os.environ
631 def __init__(
632 self,
633 *,
634 color_system: Optional[
635 Literal["auto", "standard", "256", "truecolor", "windows"]
636 ] = "auto",
637 force_terminal: Optional[bool] = None,
638 force_jupyter: Optional[bool] = None,
639 force_interactive: Optional[bool] = None,
640 soft_wrap: bool = False,
641 theme: Optional[Theme] = None,
642 stderr: bool = False,
643 file: Optional[IO[str]] = None,
644 quiet: bool = False,
645 width: Optional[int] = None,
646 height: Optional[int] = None,
647 style: Optional[StyleType] = None,
648 no_color: Optional[bool] = None,
649 tab_size: int = 8,
650 record: bool = False,
651 markup: bool = True,
652 emoji: bool = True,
653 emoji_variant: Optional[EmojiVariant] = None,
654 highlight: bool = True,
655 log_time: bool = True,
656 log_path: bool = True,
657 log_time_format: Union[str, FormatTimeCallable] = "[%X]",
658 highlighter: Optional["HighlighterType"] = ReprHighlighter(),
659 legacy_windows: Optional[bool] = None,
660 safe_box: bool = True,
661 get_datetime: Optional[Callable[[], datetime]] = None,
662 get_time: Optional[Callable[[], float]] = None,
663 _environ: Optional[Mapping[str, str]] = None,
664 ):
665 # Copy of os.environ allows us to replace it for testing
666 if _environ is not None:
667 self._environ = _environ
669 self.is_jupyter = _is_jupyter() if force_jupyter is None else force_jupyter
670 if self.is_jupyter:
671 if width is None:
672 jupyter_columns = self._environ.get("JUPYTER_COLUMNS")
673 if jupyter_columns is not None and jupyter_columns.isdigit():
674 width = int(jupyter_columns)
675 else:
676 width = JUPYTER_DEFAULT_COLUMNS
677 if height is None:
678 jupyter_lines = self._environ.get("JUPYTER_LINES")
679 if jupyter_lines is not None and jupyter_lines.isdigit():
680 height = int(jupyter_lines)
681 else:
682 height = JUPYTER_DEFAULT_LINES
684 self.tab_size = tab_size
685 self.record = record
686 self._markup = markup
687 self._emoji = emoji
688 self._emoji_variant: Optional[EmojiVariant] = emoji_variant
689 self._highlight = highlight
690 self.legacy_windows: bool = (
691 (detect_legacy_windows() and not self.is_jupyter)
692 if legacy_windows is None
693 else legacy_windows
694 )
696 if width is None:
697 columns = self._environ.get("COLUMNS")
698 if columns is not None and columns.isdigit():
699 width = int(columns) - self.legacy_windows
700 if height is None:
701 lines = self._environ.get("LINES")
702 if lines is not None and lines.isdigit():
703 height = int(lines)
705 self.soft_wrap = soft_wrap
706 self._width = width
707 self._height = height
709 self._color_system: Optional[ColorSystem]
711 self._force_terminal = None
712 if force_terminal is not None:
713 self._force_terminal = force_terminal
714 else:
715 # If FORCE_COLOR env var has any value at all, we force terminal.
716 force_color = self._environ.get("FORCE_COLOR")
717 if force_color is not None:
718 self._force_terminal = True
720 self._file = file
721 self.quiet = quiet
722 self.stderr = stderr
724 if color_system is None:
725 self._color_system = None
726 elif color_system == "auto":
727 self._color_system = self._detect_color_system()
728 else:
729 self._color_system = COLOR_SYSTEMS[color_system]
731 self._lock = threading.RLock()
732 self._log_render = LogRender(
733 show_time=log_time,
734 show_path=log_path,
735 time_format=log_time_format,
736 )
737 self.highlighter: HighlighterType = highlighter or _null_highlighter
738 self.safe_box = safe_box
739 self.get_datetime = get_datetime or datetime.now
740 self.get_time = get_time or monotonic
741 self.style = style
742 self.no_color = (
743 no_color if no_color is not None else "NO_COLOR" in self._environ
744 )
745 self.is_interactive = (
746 (self.is_terminal and not self.is_dumb_terminal)
747 if force_interactive is None
748 else force_interactive
749 )
751 self._record_buffer_lock = threading.RLock()
752 self._thread_locals = ConsoleThreadLocals(
753 theme_stack=ThemeStack(themes.DEFAULT if theme is None else theme)
754 )
755 self._record_buffer: List[Segment] = []
756 self._render_hooks: List[RenderHook] = []
757 self._live: Optional["Live"] = None
758 self._is_alt_screen = False
760 def __repr__(self) -> str:
761 return f"<console width={self.width} {str(self._color_system)}>"
763 @property
764 def file(self) -> IO[str]:
765 """Get the file object to write to."""
766 file = self._file or (sys.stderr if self.stderr else sys.stdout)
767 file = getattr(file, "rich_proxied_file", file)
768 if file is None:
769 file = NULL_FILE
770 return file
772 @file.setter
773 def file(self, new_file: IO[str]) -> None:
774 """Set a new file object."""
775 self._file = new_file
777 @property
778 def _buffer(self) -> List[Segment]:
779 """Get a thread local buffer."""
780 return self._thread_locals.buffer
782 @property
783 def _buffer_index(self) -> int:
784 """Get a thread local buffer."""
785 return self._thread_locals.buffer_index
787 @_buffer_index.setter
788 def _buffer_index(self, value: int) -> None:
789 self._thread_locals.buffer_index = value
791 @property
792 def _theme_stack(self) -> ThemeStack:
793 """Get the thread local theme stack."""
794 return self._thread_locals.theme_stack
796 def _detect_color_system(self) -> Optional[ColorSystem]:
797 """Detect color system from env vars."""
798 if self.is_jupyter:
799 return ColorSystem.TRUECOLOR
800 if not self.is_terminal or self.is_dumb_terminal:
801 return None
802 if WINDOWS: # pragma: no cover
803 if self.legacy_windows: # pragma: no cover
804 return ColorSystem.WINDOWS
805 windows_console_features = get_windows_console_features()
806 return (
807 ColorSystem.TRUECOLOR
808 if windows_console_features.truecolor
809 else ColorSystem.EIGHT_BIT
810 )
811 else:
812 color_term = self._environ.get("COLORTERM", "").strip().lower()
813 if color_term in ("truecolor", "24bit"):
814 return ColorSystem.TRUECOLOR
815 term = self._environ.get("TERM", "").strip().lower()
816 _term_name, _hyphen, colors = term.rpartition("-")
817 color_system = _TERM_COLORS.get(colors, ColorSystem.STANDARD)
818 return color_system
820 def _enter_buffer(self) -> None:
821 """Enter in to a buffer context, and buffer all output."""
822 self._buffer_index += 1
824 def _exit_buffer(self) -> None:
825 """Leave buffer context, and render content if required."""
826 self._buffer_index -= 1
827 self._check_buffer()
829 def set_live(self, live: "Live") -> None:
830 """Set Live instance. Used by Live context manager.
832 Args:
833 live (Live): Live instance using this Console.
835 Raises:
836 errors.LiveError: If this Console has a Live context currently active.
837 """
838 with self._lock:
839 if self._live is not None:
840 raise errors.LiveError("Only one live display may be active at once")
841 self._live = live
843 def clear_live(self) -> None:
844 """Clear the Live instance."""
845 with self._lock:
846 self._live = None
848 def push_render_hook(self, hook: RenderHook) -> None:
849 """Add a new render hook to the stack.
851 Args:
852 hook (RenderHook): Render hook instance.
853 """
854 with self._lock:
855 self._render_hooks.append(hook)
857 def pop_render_hook(self) -> None:
858 """Pop the last renderhook from the stack."""
859 with self._lock:
860 self._render_hooks.pop()
862 def __enter__(self) -> "Console":
863 """Own context manager to enter buffer context."""
864 self._enter_buffer()
865 return self
867 def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
868 """Exit buffer context."""
869 self._exit_buffer()
871 def begin_capture(self) -> None:
872 """Begin capturing console output. Call :meth:`end_capture` to exit capture mode and return output."""
873 self._enter_buffer()
875 def end_capture(self) -> str:
876 """End capture mode and return captured string.
878 Returns:
879 str: Console output.
880 """
881 render_result = self._render_buffer(self._buffer)
882 del self._buffer[:]
883 self._exit_buffer()
884 return render_result
886 def push_theme(self, theme: Theme, *, inherit: bool = True) -> None:
887 """Push a new theme on to the top of the stack, replacing the styles from the previous theme.
888 Generally speaking, you should call :meth:`~rich.console.Console.use_theme` to get a context manager, rather
889 than calling this method directly.
891 Args:
892 theme (Theme): A theme instance.
893 inherit (bool, optional): Inherit existing styles. Defaults to True.
894 """
895 self._theme_stack.push_theme(theme, inherit=inherit)
897 def pop_theme(self) -> None:
898 """Remove theme from top of stack, restoring previous theme."""
899 self._theme_stack.pop_theme()
901 def use_theme(self, theme: Theme, *, inherit: bool = True) -> ThemeContext:
902 """Use a different theme for the duration of the context manager.
904 Args:
905 theme (Theme): Theme instance to user.
906 inherit (bool, optional): Inherit existing console styles. Defaults to True.
908 Returns:
909 ThemeContext: [description]
910 """
911 return ThemeContext(self, theme, inherit)
913 @property
914 def color_system(self) -> Optional[str]:
915 """Get color system string.
917 Returns:
918 Optional[str]: "standard", "256" or "truecolor".
919 """
921 if self._color_system is not None:
922 return _COLOR_SYSTEMS_NAMES[self._color_system]
923 else:
924 return None
926 @property
927 def encoding(self) -> str:
928 """Get the encoding of the console file, e.g. ``"utf-8"``.
930 Returns:
931 str: A standard encoding string.
932 """
933 return (getattr(self.file, "encoding", "utf-8") or "utf-8").lower()
935 @property
936 def is_terminal(self) -> bool:
937 """Check if the console is writing to a terminal.
939 Returns:
940 bool: True if the console writing to a device capable of
941 understanding terminal codes, otherwise False.
942 """
943 if self._force_terminal is not None:
944 return self._force_terminal
946 if hasattr(sys.stdin, "__module__") and sys.stdin.__module__.startswith(
947 "idlelib"
948 ):
949 # Return False for Idle which claims to be a tty but can't handle ansi codes
950 return False
952 isatty: Optional[Callable[[], bool]] = getattr(self.file, "isatty", None)
953 try:
954 return False if isatty is None else isatty()
955 except ValueError:
956 # in some situation (at the end of a pytest run for example) isatty() can raise
957 # ValueError: I/O operation on closed file
958 # return False because we aren't in a terminal anymore
959 return False
961 @property
962 def is_dumb_terminal(self) -> bool:
963 """Detect dumb terminal.
965 Returns:
966 bool: True if writing to a dumb terminal, otherwise False.
968 """
969 _term = self._environ.get("TERM", "")
970 is_dumb = _term.lower() in ("dumb", "unknown")
971 return self.is_terminal and is_dumb
973 @property
974 def options(self) -> ConsoleOptions:
975 """Get default console options."""
976 return ConsoleOptions(
977 max_height=self.size.height,
978 size=self.size,
979 legacy_windows=self.legacy_windows,
980 min_width=1,
981 max_width=self.width,
982 encoding=self.encoding,
983 is_terminal=self.is_terminal,
984 )
986 @property
987 def size(self) -> ConsoleDimensions:
988 """Get the size of the console.
990 Returns:
991 ConsoleDimensions: A named tuple containing the dimensions.
992 """
994 if self._width is not None and self._height is not None:
995 return ConsoleDimensions(self._width - self.legacy_windows, self._height)
997 if self.is_dumb_terminal:
998 return ConsoleDimensions(80, 25)
1000 width: Optional[int] = None
1001 height: Optional[int] = None
1003 if WINDOWS: # pragma: no cover
1004 try:
1005 width, height = os.get_terminal_size()
1006 except (AttributeError, ValueError, OSError): # Probably not a terminal
1007 pass
1008 else:
1009 for file_descriptor in _STD_STREAMS:
1010 try:
1011 width, height = os.get_terminal_size(file_descriptor)
1012 except (AttributeError, ValueError, OSError):
1013 pass
1014 else:
1015 break
1017 columns = self._environ.get("COLUMNS")
1018 if columns is not None and columns.isdigit():
1019 width = int(columns)
1020 lines = self._environ.get("LINES")
1021 if lines is not None and lines.isdigit():
1022 height = int(lines)
1024 # get_terminal_size can report 0, 0 if run from pseudo-terminal
1025 width = width or 80
1026 height = height or 25
1027 return ConsoleDimensions(
1028 width - self.legacy_windows if self._width is None else self._width,
1029 height if self._height is None else self._height,
1030 )
1032 @size.setter
1033 def size(self, new_size: Tuple[int, int]) -> None:
1034 """Set a new size for the terminal.
1036 Args:
1037 new_size (Tuple[int, int]): New width and height.
1038 """
1039 width, height = new_size
1040 self._width = width
1041 self._height = height
1043 @property
1044 def width(self) -> int:
1045 """Get the width of the console.
1047 Returns:
1048 int: The width (in characters) of the console.
1049 """
1050 return self.size.width
1052 @width.setter
1053 def width(self, width: int) -> None:
1054 """Set width.
1056 Args:
1057 width (int): New width.
1058 """
1059 self._width = width
1061 @property
1062 def height(self) -> int:
1063 """Get the height of the console.
1065 Returns:
1066 int: The height (in lines) of the console.
1067 """
1068 return self.size.height
1070 @height.setter
1071 def height(self, height: int) -> None:
1072 """Set height.
1074 Args:
1075 height (int): new height.
1076 """
1077 self._height = height
1079 def bell(self) -> None:
1080 """Play a 'bell' sound (if supported by the terminal)."""
1081 self.control(Control.bell())
1083 def capture(self) -> Capture:
1084 """A context manager to *capture* the result of print() or log() in a string,
1085 rather than writing it to the console.
1087 Example:
1088 >>> from rich.console import Console
1089 >>> console = Console()
1090 >>> with console.capture() as capture:
1091 ... console.print("[bold magenta]Hello World[/]")
1092 >>> print(capture.get())
1094 Returns:
1095 Capture: Context manager with disables writing to the terminal.
1096 """
1097 capture = Capture(self)
1098 return capture
1100 def pager(
1101 self, pager: Optional[Pager] = None, styles: bool = False, links: bool = False
1102 ) -> PagerContext:
1103 """A context manager to display anything printed within a "pager". The pager application
1104 is defined by the system and will typically support at least pressing a key to scroll.
1106 Args:
1107 pager (Pager, optional): A pager object, or None to use :class:`~rich.pager.SystemPager`. Defaults to None.
1108 styles (bool, optional): Show styles in pager. Defaults to False.
1109 links (bool, optional): Show links in pager. Defaults to False.
1111 Example:
1112 >>> from rich.console import Console
1113 >>> from rich.__main__ import make_test_card
1114 >>> console = Console()
1115 >>> with console.pager():
1116 console.print(make_test_card())
1118 Returns:
1119 PagerContext: A context manager.
1120 """
1121 return PagerContext(self, pager=pager, styles=styles, links=links)
1123 def line(self, count: int = 1) -> None:
1124 """Write new line(s).
1126 Args:
1127 count (int, optional): Number of new lines. Defaults to 1.
1128 """
1130 assert count >= 0, "count must be >= 0"
1131 self.print(NewLine(count))
1133 def clear(self, home: bool = True) -> None:
1134 """Clear the screen.
1136 Args:
1137 home (bool, optional): Also move the cursor to 'home' position. Defaults to True.
1138 """
1139 if home:
1140 self.control(Control.clear(), Control.home())
1141 else:
1142 self.control(Control.clear())
1144 def status(
1145 self,
1146 status: RenderableType,
1147 *,
1148 spinner: str = "dots",
1149 spinner_style: str = "status.spinner",
1150 speed: float = 1.0,
1151 refresh_per_second: float = 12.5,
1152 ) -> "Status":
1153 """Display a status and spinner.
1155 Args:
1156 status (RenderableType): A status renderable (str or Text typically).
1157 spinner (str, optional): Name of spinner animation (see python -m rich.spinner). Defaults to "dots".
1158 spinner_style (StyleType, optional): Style of spinner. Defaults to "status.spinner".
1159 speed (float, optional): Speed factor for spinner animation. Defaults to 1.0.
1160 refresh_per_second (float, optional): Number of refreshes per second. Defaults to 12.5.
1162 Returns:
1163 Status: A Status object that may be used as a context manager.
1164 """
1165 from .status import Status
1167 status_renderable = Status(
1168 status,
1169 console=self,
1170 spinner=spinner,
1171 spinner_style=spinner_style,
1172 speed=speed,
1173 refresh_per_second=refresh_per_second,
1174 )
1175 return status_renderable
1177 def show_cursor(self, show: bool = True) -> bool:
1178 """Show or hide the cursor.
1180 Args:
1181 show (bool, optional): Set visibility of the cursor.
1182 """
1183 if self.is_terminal:
1184 self.control(Control.show_cursor(show))
1185 return True
1186 return False
1188 def set_alt_screen(self, enable: bool = True) -> bool:
1189 """Enables alternative screen mode.
1191 Note, if you enable this mode, you should ensure that is disabled before
1192 the application exits. See :meth:`~rich.Console.screen` for a context manager
1193 that handles this for you.
1195 Args:
1196 enable (bool, optional): Enable (True) or disable (False) alternate screen. Defaults to True.
1198 Returns:
1199 bool: True if the control codes were written.
1201 """
1202 changed = False
1203 if self.is_terminal and not self.legacy_windows:
1204 self.control(Control.alt_screen(enable))
1205 changed = True
1206 self._is_alt_screen = enable
1207 return changed
1209 @property
1210 def is_alt_screen(self) -> bool:
1211 """Check if the alt screen was enabled.
1213 Returns:
1214 bool: True if the alt screen was enabled, otherwise False.
1215 """
1216 return self._is_alt_screen
1218 def set_window_title(self, title: str) -> bool:
1219 """Set the title of the console terminal window.
1221 Warning: There is no means within Rich of "resetting" the window title to its
1222 previous value, meaning the title you set will persist even after your application
1223 exits.
1225 ``fish`` shell resets the window title before and after each command by default,
1226 negating this issue. Windows Terminal and command prompt will also reset the title for you.
1227 Most other shells and terminals, however, do not do this.
1229 Some terminals may require configuration changes before you can set the title.
1230 Some terminals may not support setting the title at all.
1232 Other software (including the terminal itself, the shell, custom prompts, plugins, etc.)
1233 may also set the terminal window title. This could result in whatever value you write
1234 using this method being overwritten.
1236 Args:
1237 title (str): The new title of the terminal window.
1239 Returns:
1240 bool: True if the control code to change the terminal title was
1241 written, otherwise False. Note that a return value of True
1242 does not guarantee that the window title has actually changed,
1243 since the feature may be unsupported/disabled in some terminals.
1244 """
1245 if self.is_terminal:
1246 self.control(Control.title(title))
1247 return True
1248 return False
1250 def screen(
1251 self, hide_cursor: bool = True, style: Optional[StyleType] = None
1252 ) -> "ScreenContext":
1253 """Context manager to enable and disable 'alternative screen' mode.
1255 Args:
1256 hide_cursor (bool, optional): Also hide the cursor. Defaults to False.
1257 style (Style, optional): Optional style for screen. Defaults to None.
1259 Returns:
1260 ~ScreenContext: Context which enables alternate screen on enter, and disables it on exit.
1261 """
1262 return ScreenContext(self, hide_cursor=hide_cursor, style=style or "")
1264 def measure(
1265 self, renderable: RenderableType, *, options: Optional[ConsoleOptions] = None
1266 ) -> Measurement:
1267 """Measure a renderable. Returns a :class:`~rich.measure.Measurement` object which contains
1268 information regarding the number of characters required to print the renderable.
1270 Args:
1271 renderable (RenderableType): Any renderable or string.
1272 options (Optional[ConsoleOptions], optional): Options to use when measuring, or None
1273 to use default options. Defaults to None.
1275 Returns:
1276 Measurement: A measurement of the renderable.
1277 """
1278 measurement = Measurement.get(self, options or self.options, renderable)
1279 return measurement
1281 def render(
1282 self, renderable: RenderableType, options: Optional[ConsoleOptions] = None
1283 ) -> Iterable[Segment]:
1284 """Render an object in to an iterable of `Segment` instances.
1286 This method contains the logic for rendering objects with the console protocol.
1287 You are unlikely to need to use it directly, unless you are extending the library.
1289 Args:
1290 renderable (RenderableType): An object supporting the console protocol, or
1291 an object that may be converted to a string.
1292 options (ConsoleOptions, optional): An options object, or None to use self.options. Defaults to None.
1294 Returns:
1295 Iterable[Segment]: An iterable of segments that may be rendered.
1296 """
1298 _options = options or self.options
1299 if _options.max_width < 1:
1300 # No space to render anything. This prevents potential recursion errors.
1301 return
1302 render_iterable: RenderResult
1304 renderable = rich_cast(renderable)
1305 if hasattr(renderable, "__rich_console__") and not isclass(renderable):
1306 render_iterable = renderable.__rich_console__(self, _options) # type: ignore[union-attr]
1307 elif isinstance(renderable, str):
1308 text_renderable = self.render_str(
1309 renderable, highlight=_options.highlight, markup=_options.markup
1310 )
1311 render_iterable = text_renderable.__rich_console__(self, _options)
1312 else:
1313 raise errors.NotRenderableError(
1314 f"Unable to render {renderable!r}; "
1315 "A str, Segment or object with __rich_console__ method is required"
1316 )
1318 try:
1319 iter_render = iter(render_iterable)
1320 except TypeError:
1321 raise errors.NotRenderableError(
1322 f"object {render_iterable!r} is not renderable"
1323 )
1324 _Segment = Segment
1325 _options = _options.reset_height()
1326 for render_output in iter_render:
1327 if isinstance(render_output, _Segment):
1328 yield render_output
1329 else:
1330 yield from self.render(render_output, _options)
1332 def render_lines(
1333 self,
1334 renderable: RenderableType,
1335 options: Optional[ConsoleOptions] = None,
1336 *,
1337 style: Optional[Style] = None,
1338 pad: bool = True,
1339 new_lines: bool = False,
1340 ) -> List[List[Segment]]:
1341 """Render objects in to a list of lines.
1343 The output of render_lines is useful when further formatting of rendered console text
1344 is required, such as the Panel class which draws a border around any renderable object.
1346 Args:
1347 renderable (RenderableType): Any object renderable in the console.
1348 options (Optional[ConsoleOptions], optional): Console options, or None to use self.options. Default to ``None``.
1349 style (Style, optional): Optional style to apply to renderables. Defaults to ``None``.
1350 pad (bool, optional): Pad lines shorter than render width. Defaults to ``True``.
1351 new_lines (bool, optional): Include "\n" characters at end of lines.
1353 Returns:
1354 List[List[Segment]]: A list of lines, where a line is a list of Segment objects.
1355 """
1356 with self._lock:
1357 render_options = options or self.options
1358 _rendered = self.render(renderable, render_options)
1359 if style:
1360 _rendered = Segment.apply_style(_rendered, style)
1362 render_height = render_options.height
1363 if render_height is not None:
1364 render_height = max(0, render_height)
1366 lines = list(
1367 islice(
1368 Segment.split_and_crop_lines(
1369 _rendered,
1370 render_options.max_width,
1371 include_new_lines=new_lines,
1372 pad=pad,
1373 style=style,
1374 ),
1375 None,
1376 render_height,
1377 )
1378 )
1379 if render_options.height is not None:
1380 extra_lines = render_options.height - len(lines)
1381 if extra_lines > 0:
1382 pad_line = [
1383 [Segment(" " * render_options.max_width, style), Segment("\n")]
1384 if new_lines
1385 else [Segment(" " * render_options.max_width, style)]
1386 ]
1387 lines.extend(pad_line * extra_lines)
1389 return lines
1391 def render_str(
1392 self,
1393 text: str,
1394 *,
1395 style: Union[str, Style] = "",
1396 justify: Optional[JustifyMethod] = None,
1397 overflow: Optional[OverflowMethod] = None,
1398 emoji: Optional[bool] = None,
1399 markup: Optional[bool] = None,
1400 highlight: Optional[bool] = None,
1401 highlighter: Optional[HighlighterType] = None,
1402 ) -> "Text":
1403 """Convert a string to a Text instance. This is called automatically if
1404 you print or log a string.
1406 Args:
1407 text (str): Text to render.
1408 style (Union[str, Style], optional): Style to apply to rendered text.
1409 justify (str, optional): Justify method: "default", "left", "center", "full", or "right". Defaults to ``None``.
1410 overflow (str, optional): Overflow method: "crop", "fold", or "ellipsis". Defaults to ``None``.
1411 emoji (Optional[bool], optional): Enable emoji, or ``None`` to use Console default.
1412 markup (Optional[bool], optional): Enable markup, or ``None`` to use Console default.
1413 highlight (Optional[bool], optional): Enable highlighting, or ``None`` to use Console default.
1414 highlighter (HighlighterType, optional): Optional highlighter to apply.
1415 Returns:
1416 ConsoleRenderable: Renderable object.
1418 """
1419 emoji_enabled = emoji or (emoji is None and self._emoji)
1420 markup_enabled = markup or (markup is None and self._markup)
1421 highlight_enabled = highlight or (highlight is None and self._highlight)
1423 if markup_enabled:
1424 rich_text = render_markup(
1425 text,
1426 style=style,
1427 emoji=emoji_enabled,
1428 emoji_variant=self._emoji_variant,
1429 )
1430 rich_text.justify = justify
1431 rich_text.overflow = overflow
1432 else:
1433 rich_text = Text(
1434 _emoji_replace(text, default_variant=self._emoji_variant)
1435 if emoji_enabled
1436 else text,
1437 justify=justify,
1438 overflow=overflow,
1439 style=style,
1440 )
1442 _highlighter = (highlighter or self.highlighter) if highlight_enabled else None
1443 if _highlighter is not None:
1444 highlight_text = _highlighter(str(rich_text))
1445 highlight_text.copy_styles(rich_text)
1446 return highlight_text
1448 return rich_text
1450 def get_style(
1451 self, name: Union[str, Style], *, default: Optional[Union[Style, str]] = None
1452 ) -> Style:
1453 """Get a Style instance by its theme name or parse a definition.
1455 Args:
1456 name (str): The name of a style or a style definition.
1458 Returns:
1459 Style: A Style object.
1461 Raises:
1462 MissingStyle: If no style could be parsed from name.
1464 """
1465 if isinstance(name, Style):
1466 return name
1468 try:
1469 style = self._theme_stack.get(name)
1470 if style is None:
1471 style = Style.parse(name)
1472 return style.copy() if style.link else style
1473 except errors.StyleSyntaxError as error:
1474 if default is not None:
1475 return self.get_style(default)
1476 raise errors.MissingStyle(
1477 f"Failed to get style {name!r}; {error}"
1478 ) from None
1480 def _collect_renderables(
1481 self,
1482 objects: Iterable[Any],
1483 sep: str,
1484 end: str,
1485 *,
1486 justify: Optional[JustifyMethod] = None,
1487 emoji: Optional[bool] = None,
1488 markup: Optional[bool] = None,
1489 highlight: Optional[bool] = None,
1490 ) -> List[ConsoleRenderable]:
1491 """Combine a number of renderables and text into one renderable.
1493 Args:
1494 objects (Iterable[Any]): Anything that Rich can render.
1495 sep (str): String to write between print data.
1496 end (str): String to write at end of print data.
1497 justify (str, optional): One of "left", "right", "center", or "full". Defaults to ``None``.
1498 emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default.
1499 markup (Optional[bool], optional): Enable markup, or ``None`` to use console default.
1500 highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use console default.
1502 Returns:
1503 List[ConsoleRenderable]: A list of things to render.
1504 """
1505 renderables: List[ConsoleRenderable] = []
1506 _append = renderables.append
1507 text: List[Text] = []
1508 append_text = text.append
1510 append = _append
1511 if justify in ("left", "center", "right"):
1513 def align_append(renderable: RenderableType) -> None:
1514 _append(Align(renderable, cast(AlignMethod, justify)))
1516 append = align_append
1518 _highlighter: HighlighterType = _null_highlighter
1519 if highlight or (highlight is None and self._highlight):
1520 _highlighter = self.highlighter
1522 def check_text() -> None:
1523 if text:
1524 sep_text = Text(sep, justify=justify, end=end)
1525 append(sep_text.join(text))
1526 del text[:]
1528 for renderable in objects:
1529 renderable = rich_cast(renderable)
1530 if isinstance(renderable, str):
1531 append_text(
1532 self.render_str(
1533 renderable, emoji=emoji, markup=markup, highlighter=_highlighter
1534 )
1535 )
1536 elif isinstance(renderable, Text):
1537 append_text(renderable)
1538 elif isinstance(renderable, ConsoleRenderable):
1539 check_text()
1540 append(renderable)
1541 elif is_expandable(renderable):
1542 check_text()
1543 append(Pretty(renderable, highlighter=_highlighter))
1544 else:
1545 append_text(_highlighter(str(renderable)))
1547 check_text()
1549 if self.style is not None:
1550 style = self.get_style(self.style)
1551 renderables = [Styled(renderable, style) for renderable in renderables]
1553 return renderables
1555 def rule(
1556 self,
1557 title: TextType = "",
1558 *,
1559 characters: str = "─",
1560 style: Union[str, Style] = "rule.line",
1561 align: AlignMethod = "center",
1562 ) -> None:
1563 """Draw a line with optional centered title.
1565 Args:
1566 title (str, optional): Text to render over the rule. Defaults to "".
1567 characters (str, optional): Character(s) to form the line. Defaults to "─".
1568 style (str, optional): Style of line. Defaults to "rule.line".
1569 align (str, optional): How to align the title, one of "left", "center", or "right". Defaults to "center".
1570 """
1571 from .rule import Rule
1573 rule = Rule(title=title, characters=characters, style=style, align=align)
1574 self.print(rule)
1576 def control(self, *control: Control) -> None:
1577 """Insert non-printing control codes.
1579 Args:
1580 control_codes (str): Control codes, such as those that may move the cursor.
1581 """
1582 if not self.is_dumb_terminal:
1583 with self:
1584 self._buffer.extend(_control.segment for _control in control)
1586 def out(
1587 self,
1588 *objects: Any,
1589 sep: str = " ",
1590 end: str = "\n",
1591 style: Optional[Union[str, Style]] = None,
1592 highlight: Optional[bool] = None,
1593 ) -> None:
1594 """Output to the terminal. This is a low-level way of writing to the terminal which unlike
1595 :meth:`~rich.console.Console.print` won't pretty print, wrap text, or apply markup, but will
1596 optionally apply highlighting and a basic style.
1598 Args:
1599 sep (str, optional): String to write between print data. Defaults to " ".
1600 end (str, optional): String to write at end of print data. Defaults to "\\\\n".
1601 style (Union[str, Style], optional): A style to apply to output. Defaults to None.
1602 highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use
1603 console default. Defaults to ``None``.
1604 """
1605 raw_output: str = sep.join(str(_object) for _object in objects)
1606 self.print(
1607 raw_output,
1608 style=style,
1609 highlight=highlight,
1610 emoji=False,
1611 markup=False,
1612 no_wrap=True,
1613 overflow="ignore",
1614 crop=False,
1615 end=end,
1616 )
1618 def print(
1619 self,
1620 *objects: Any,
1621 sep: str = " ",
1622 end: str = "\n",
1623 style: Optional[Union[str, Style]] = None,
1624 justify: Optional[JustifyMethod] = None,
1625 overflow: Optional[OverflowMethod] = None,
1626 no_wrap: Optional[bool] = None,
1627 emoji: Optional[bool] = None,
1628 markup: Optional[bool] = None,
1629 highlight: Optional[bool] = None,
1630 width: Optional[int] = None,
1631 height: Optional[int] = None,
1632 crop: bool = True,
1633 soft_wrap: Optional[bool] = None,
1634 new_line_start: bool = False,
1635 ) -> None:
1636 """Print to the console.
1638 Args:
1639 objects (positional args): Objects to log to the terminal.
1640 sep (str, optional): String to write between print data. Defaults to " ".
1641 end (str, optional): String to write at end of print data. Defaults to "\\\\n".
1642 style (Union[str, Style], optional): A style to apply to output. Defaults to None.
1643 justify (str, optional): Justify method: "default", "left", "right", "center", or "full". Defaults to ``None``.
1644 overflow (str, optional): Overflow method: "ignore", "crop", "fold", or "ellipsis". Defaults to None.
1645 no_wrap (Optional[bool], optional): Disable word wrapping. Defaults to None.
1646 emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default. Defaults to ``None``.
1647 markup (Optional[bool], optional): Enable markup, or ``None`` to use console default. Defaults to ``None``.
1648 highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use console default. Defaults to ``None``.
1649 width (Optional[int], optional): Width of output, or ``None`` to auto-detect. Defaults to ``None``.
1650 crop (Optional[bool], optional): Crop output to width of terminal. Defaults to True.
1651 soft_wrap (bool, optional): Enable soft wrap mode which disables word wrapping and cropping of text or ``None`` for
1652 Console default. Defaults to ``None``.
1653 new_line_start (bool, False): Insert a new line at the start if the output contains more than one line. Defaults to ``False``.
1654 """
1655 if not objects:
1656 objects = (NewLine(),)
1658 if soft_wrap is None:
1659 soft_wrap = self.soft_wrap
1660 if soft_wrap:
1661 if no_wrap is None:
1662 no_wrap = True
1663 if overflow is None:
1664 overflow = "ignore"
1665 crop = False
1666 render_hooks = self._render_hooks[:]
1667 with self:
1668 renderables = self._collect_renderables(
1669 objects,
1670 sep,
1671 end,
1672 justify=justify,
1673 emoji=emoji,
1674 markup=markup,
1675 highlight=highlight,
1676 )
1677 for hook in render_hooks:
1678 renderables = hook.process_renderables(renderables)
1679 render_options = self.options.update(
1680 justify=justify,
1681 overflow=overflow,
1682 width=min(width, self.width) if width is not None else NO_CHANGE,
1683 height=height,
1684 no_wrap=no_wrap,
1685 markup=markup,
1686 highlight=highlight,
1687 )
1689 new_segments: List[Segment] = []
1690 extend = new_segments.extend
1691 render = self.render
1692 if style is None:
1693 for renderable in renderables:
1694 extend(render(renderable, render_options))
1695 else:
1696 for renderable in renderables:
1697 extend(
1698 Segment.apply_style(
1699 render(renderable, render_options), self.get_style(style)
1700 )
1701 )
1702 if new_line_start:
1703 if (
1704 len("".join(segment.text for segment in new_segments).splitlines())
1705 > 1
1706 ):
1707 new_segments.insert(0, Segment.line())
1708 if crop:
1709 buffer_extend = self._buffer.extend
1710 for line in Segment.split_and_crop_lines(
1711 new_segments, self.width, pad=False
1712 ):
1713 buffer_extend(line)
1714 else:
1715 self._buffer.extend(new_segments)
1717 def print_json(
1718 self,
1719 json: Optional[str] = None,
1720 *,
1721 data: Any = None,
1722 indent: Union[None, int, str] = 2,
1723 highlight: bool = True,
1724 skip_keys: bool = False,
1725 ensure_ascii: bool = False,
1726 check_circular: bool = True,
1727 allow_nan: bool = True,
1728 default: Optional[Callable[[Any], Any]] = None,
1729 sort_keys: bool = False,
1730 ) -> None:
1731 """Pretty prints JSON. Output will be valid JSON.
1733 Args:
1734 json (Optional[str]): A string containing JSON.
1735 data (Any): If json is not supplied, then encode this data.
1736 indent (Union[None, int, str], optional): Number of spaces to indent. Defaults to 2.
1737 highlight (bool, optional): Enable highlighting of output: Defaults to True.
1738 skip_keys (bool, optional): Skip keys not of a basic type. Defaults to False.
1739 ensure_ascii (bool, optional): Escape all non-ascii characters. Defaults to False.
1740 check_circular (bool, optional): Check for circular references. Defaults to True.
1741 allow_nan (bool, optional): Allow NaN and Infinity values. Defaults to True.
1742 default (Callable, optional): A callable that converts values that can not be encoded
1743 in to something that can be JSON encoded. Defaults to None.
1744 sort_keys (bool, optional): Sort dictionary keys. Defaults to False.
1745 """
1746 from rich.json import JSON
1748 if json is None:
1749 json_renderable = JSON.from_data(
1750 data,
1751 indent=indent,
1752 highlight=highlight,
1753 skip_keys=skip_keys,
1754 ensure_ascii=ensure_ascii,
1755 check_circular=check_circular,
1756 allow_nan=allow_nan,
1757 default=default,
1758 sort_keys=sort_keys,
1759 )
1760 else:
1761 if not isinstance(json, str):
1762 raise TypeError(
1763 f"json must be str. Did you mean print_json(data={json!r}) ?"
1764 )
1765 json_renderable = JSON(
1766 json,
1767 indent=indent,
1768 highlight=highlight,
1769 skip_keys=skip_keys,
1770 ensure_ascii=ensure_ascii,
1771 check_circular=check_circular,
1772 allow_nan=allow_nan,
1773 default=default,
1774 sort_keys=sort_keys,
1775 )
1776 self.print(json_renderable, soft_wrap=True)
1778 def update_screen(
1779 self,
1780 renderable: RenderableType,
1781 *,
1782 region: Optional[Region] = None,
1783 options: Optional[ConsoleOptions] = None,
1784 ) -> None:
1785 """Update the screen at a given offset.
1787 Args:
1788 renderable (RenderableType): A Rich renderable.
1789 region (Region, optional): Region of screen to update, or None for entire screen. Defaults to None.
1790 x (int, optional): x offset. Defaults to 0.
1791 y (int, optional): y offset. Defaults to 0.
1793 Raises:
1794 errors.NoAltScreen: If the Console isn't in alt screen mode.
1796 """
1797 if not self.is_alt_screen:
1798 raise errors.NoAltScreen("Alt screen must be enabled to call update_screen")
1799 render_options = options or self.options
1800 if region is None:
1801 x = y = 0
1802 render_options = render_options.update_dimensions(
1803 render_options.max_width, render_options.height or self.height
1804 )
1805 else:
1806 x, y, width, height = region
1807 render_options = render_options.update_dimensions(width, height)
1809 lines = self.render_lines(renderable, options=render_options)
1810 self.update_screen_lines(lines, x, y)
1812 def update_screen_lines(
1813 self, lines: List[List[Segment]], x: int = 0, y: int = 0
1814 ) -> None:
1815 """Update lines of the screen at a given offset.
1817 Args:
1818 lines (List[List[Segment]]): Rendered lines (as produced by :meth:`~rich.Console.render_lines`).
1819 x (int, optional): x offset (column no). Defaults to 0.
1820 y (int, optional): y offset (column no). Defaults to 0.
1822 Raises:
1823 errors.NoAltScreen: If the Console isn't in alt screen mode.
1824 """
1825 if not self.is_alt_screen:
1826 raise errors.NoAltScreen("Alt screen must be enabled to call update_screen")
1827 screen_update = ScreenUpdate(lines, x, y)
1828 segments = self.render(screen_update)
1829 self._buffer.extend(segments)
1830 self._check_buffer()
1832 def print_exception(
1833 self,
1834 *,
1835 width: Optional[int] = 100,
1836 extra_lines: int = 3,
1837 theme: Optional[str] = None,
1838 word_wrap: bool = False,
1839 show_locals: bool = False,
1840 suppress: Iterable[Union[str, ModuleType]] = (),
1841 max_frames: int = 100,
1842 ) -> None:
1843 """Prints a rich render of the last exception and traceback.
1845 Args:
1846 width (Optional[int], optional): Number of characters used to render code. Defaults to 100.
1847 extra_lines (int, optional): Additional lines of code to render. Defaults to 3.
1848 theme (str, optional): Override pygments theme used in traceback
1849 word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False.
1850 show_locals (bool, optional): Enable display of local variables. Defaults to False.
1851 suppress (Iterable[Union[str, ModuleType]]): Optional sequence of modules or paths to exclude from traceback.
1852 max_frames (int): Maximum number of frames to show in a traceback, 0 for no maximum. Defaults to 100.
1853 """
1854 from .traceback import Traceback
1856 traceback = Traceback(
1857 width=width,
1858 extra_lines=extra_lines,
1859 theme=theme,
1860 word_wrap=word_wrap,
1861 show_locals=show_locals,
1862 suppress=suppress,
1863 max_frames=max_frames,
1864 )
1865 self.print(traceback)
1867 @staticmethod
1868 def _caller_frame_info(
1869 offset: int,
1870 currentframe: Callable[[], Optional[FrameType]] = inspect.currentframe,
1871 ) -> Tuple[str, int, Dict[str, Any]]:
1872 """Get caller frame information.
1874 Args:
1875 offset (int): the caller offset within the current frame stack.
1876 currentframe (Callable[[], Optional[FrameType]], optional): the callable to use to
1877 retrieve the current frame. Defaults to ``inspect.currentframe``.
1879 Returns:
1880 Tuple[str, int, Dict[str, Any]]: A tuple containing the filename, the line number and
1881 the dictionary of local variables associated with the caller frame.
1883 Raises:
1884 RuntimeError: If the stack offset is invalid.
1885 """
1886 # Ignore the frame of this local helper
1887 offset += 1
1889 frame = currentframe()
1890 if frame is not None:
1891 # Use the faster currentframe where implemented
1892 while offset and frame is not None:
1893 frame = frame.f_back
1894 offset -= 1
1895 assert frame is not None
1896 return frame.f_code.co_filename, frame.f_lineno, frame.f_locals
1897 else:
1898 # Fallback to the slower stack
1899 frame_info = inspect.stack()[offset]
1900 return frame_info.filename, frame_info.lineno, frame_info.frame.f_locals
1902 def log(
1903 self,
1904 *objects: Any,
1905 sep: str = " ",
1906 end: str = "\n",
1907 style: Optional[Union[str, Style]] = None,
1908 justify: Optional[JustifyMethod] = None,
1909 emoji: Optional[bool] = None,
1910 markup: Optional[bool] = None,
1911 highlight: Optional[bool] = None,
1912 log_locals: bool = False,
1913 _stack_offset: int = 1,
1914 ) -> None:
1915 """Log rich content to the terminal.
1917 Args:
1918 objects (positional args): Objects to log to the terminal.
1919 sep (str, optional): String to write between print data. Defaults to " ".
1920 end (str, optional): String to write at end of print data. Defaults to "\\\\n".
1921 style (Union[str, Style], optional): A style to apply to output. Defaults to None.
1922 justify (str, optional): One of "left", "right", "center", or "full". Defaults to ``None``.
1923 overflow (str, optional): Overflow method: "crop", "fold", or "ellipsis". Defaults to None.
1924 emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default. Defaults to None.
1925 markup (Optional[bool], optional): Enable markup, or ``None`` to use console default. Defaults to None.
1926 highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use console default. Defaults to None.
1927 log_locals (bool, optional): Boolean to enable logging of locals where ``log()``
1928 was called. Defaults to False.
1929 _stack_offset (int, optional): Offset of caller from end of call stack. Defaults to 1.
1930 """
1931 if not objects:
1932 objects = (NewLine(),)
1934 render_hooks = self._render_hooks[:]
1936 with self:
1937 renderables = self._collect_renderables(
1938 objects,
1939 sep,
1940 end,
1941 justify=justify,
1942 emoji=emoji,
1943 markup=markup,
1944 highlight=highlight,
1945 )
1946 if style is not None:
1947 renderables = [Styled(renderable, style) for renderable in renderables]
1949 filename, line_no, locals = self._caller_frame_info(_stack_offset)
1950 link_path = None if filename.startswith("<") else os.path.abspath(filename)
1951 path = filename.rpartition(os.sep)[-1]
1952 if log_locals:
1953 locals_map = {
1954 key: value
1955 for key, value in locals.items()
1956 if not key.startswith("__")
1957 }
1958 renderables.append(render_scope(locals_map, title="[i]locals"))
1960 renderables = [
1961 self._log_render(
1962 self,
1963 renderables,
1964 log_time=self.get_datetime(),
1965 path=path,
1966 line_no=line_no,
1967 link_path=link_path,
1968 )
1969 ]
1970 for hook in render_hooks:
1971 renderables = hook.process_renderables(renderables)
1972 new_segments: List[Segment] = []
1973 extend = new_segments.extend
1974 render = self.render
1975 render_options = self.options
1976 for renderable in renderables:
1977 extend(render(renderable, render_options))
1978 buffer_extend = self._buffer.extend
1979 for line in Segment.split_and_crop_lines(
1980 new_segments, self.width, pad=False
1981 ):
1982 buffer_extend(line)
1984 def _check_buffer(self) -> None:
1985 """Check if the buffer may be rendered. Render it if it can (e.g. Console.quiet is False)
1986 Rendering is supported on Windows, Unix and Jupyter environments. For
1987 legacy Windows consoles, the win32 API is called directly.
1988 This method will also record what it renders if recording is enabled via Console.record.
1989 """
1990 if self.quiet:
1991 del self._buffer[:]
1992 return
1993 with self._lock:
1994 if self.record:
1995 with self._record_buffer_lock:
1996 self._record_buffer.extend(self._buffer[:])
1998 if self._buffer_index == 0:
2000 if self.is_jupyter: # pragma: no cover
2001 from .jupyter import display
2003 display(self._buffer, self._render_buffer(self._buffer[:]))
2004 del self._buffer[:]
2005 else:
2006 if WINDOWS:
2007 use_legacy_windows_render = False
2008 if self.legacy_windows:
2009 try:
2010 use_legacy_windows_render = (
2011 self.file.fileno() in _STD_STREAMS_OUTPUT
2012 )
2013 except (ValueError, io.UnsupportedOperation):
2014 pass
2016 if use_legacy_windows_render:
2017 from rich._win32_console import LegacyWindowsTerm
2018 from rich._windows_renderer import legacy_windows_render
2020 buffer = self._buffer[:]
2021 if self.no_color and self._color_system:
2022 buffer = list(Segment.remove_color(buffer))
2024 legacy_windows_render(buffer, LegacyWindowsTerm(self.file))
2025 else:
2026 # Either a non-std stream on legacy Windows, or modern Windows.
2027 text = self._render_buffer(self._buffer[:])
2028 # https://bugs.python.org/issue37871
2029 write = self.file.write
2030 for line in text.splitlines(True):
2031 try:
2032 write(line)
2033 except UnicodeEncodeError as error:
2034 error.reason = f"{error.reason}\n*** You may need to add PYTHONIOENCODING=utf-8 to your environment ***"
2035 raise
2036 else:
2037 text = self._render_buffer(self._buffer[:])
2038 try:
2039 self.file.write(text)
2040 except UnicodeEncodeError as error:
2041 error.reason = f"{error.reason}\n*** You may need to add PYTHONIOENCODING=utf-8 to your environment ***"
2042 raise
2044 self.file.flush()
2045 del self._buffer[:]
2047 def _render_buffer(self, buffer: Iterable[Segment]) -> str:
2048 """Render buffered output, and clear buffer."""
2049 output: List[str] = []
2050 append = output.append
2051 color_system = self._color_system
2052 legacy_windows = self.legacy_windows
2053 not_terminal = not self.is_terminal
2054 if self.no_color and color_system:
2055 buffer = Segment.remove_color(buffer)
2056 for text, style, control in buffer:
2057 if style:
2058 append(
2059 style.render(
2060 text,
2061 color_system=color_system,
2062 legacy_windows=legacy_windows,
2063 )
2064 )
2065 elif not (not_terminal and control):
2066 append(text)
2068 rendered = "".join(output)
2069 return rendered
2071 def input(
2072 self,
2073 prompt: TextType = "",
2074 *,
2075 markup: bool = True,
2076 emoji: bool = True,
2077 password: bool = False,
2078 stream: Optional[TextIO] = None,
2079 ) -> str:
2080 """Displays a prompt and waits for input from the user. The prompt may contain color / style.
2082 It works in the same way as Python's builtin :func:`input` function and provides elaborate line editing and history features if Python's builtin :mod:`readline` module is previously loaded.
2084 Args:
2085 prompt (Union[str, Text]): Text to render in the prompt.
2086 markup (bool, optional): Enable console markup (requires a str prompt). Defaults to True.
2087 emoji (bool, optional): Enable emoji (requires a str prompt). Defaults to True.
2088 password: (bool, optional): Hide typed text. Defaults to False.
2089 stream: (TextIO, optional): Optional file to read input from (rather than stdin). Defaults to None.
2091 Returns:
2092 str: Text read from stdin.
2093 """
2094 if prompt:
2095 self.print(prompt, markup=markup, emoji=emoji, end="")
2096 if password:
2097 result = getpass("", stream=stream)
2098 else:
2099 if stream:
2100 result = stream.readline()
2101 else:
2102 result = input()
2103 return result
2105 def export_text(self, *, clear: bool = True, styles: bool = False) -> str:
2106 """Generate text from console contents (requires record=True argument in constructor).
2108 Args:
2109 clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``.
2110 styles (bool, optional): If ``True``, ansi escape codes will be included. ``False`` for plain text.
2111 Defaults to ``False``.
2113 Returns:
2114 str: String containing console contents.
2116 """
2117 assert (
2118 self.record
2119 ), "To export console contents set record=True in the constructor or instance"
2121 with self._record_buffer_lock:
2122 if styles:
2123 text = "".join(
2124 (style.render(text) if style else text)
2125 for text, style, _ in self._record_buffer
2126 )
2127 else:
2128 text = "".join(
2129 segment.text
2130 for segment in self._record_buffer
2131 if not segment.control
2132 )
2133 if clear:
2134 del self._record_buffer[:]
2135 return text
2137 def save_text(self, path: str, *, clear: bool = True, styles: bool = False) -> None:
2138 """Generate text from console and save to a given location (requires record=True argument in constructor).
2140 Args:
2141 path (str): Path to write text files.
2142 clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``.
2143 styles (bool, optional): If ``True``, ansi style codes will be included. ``False`` for plain text.
2144 Defaults to ``False``.
2146 """
2147 text = self.export_text(clear=clear, styles=styles)
2148 with open(path, "wt", encoding="utf-8") as write_file:
2149 write_file.write(text)
2151 def export_html(
2152 self,
2153 *,
2154 theme: Optional[TerminalTheme] = None,
2155 clear: bool = True,
2156 code_format: Optional[str] = None,
2157 inline_styles: bool = False,
2158 ) -> str:
2159 """Generate HTML from console contents (requires record=True argument in constructor).
2161 Args:
2162 theme (TerminalTheme, optional): TerminalTheme object containing console colors.
2163 clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``.
2164 code_format (str, optional): Format string to render HTML. In addition to '{foreground}',
2165 '{background}', and '{code}', should contain '{stylesheet}' if inline_styles is ``False``.
2166 inline_styles (bool, optional): If ``True`` styles will be inlined in to spans, which makes files
2167 larger but easier to cut and paste markup. If ``False``, styles will be embedded in a style tag.
2168 Defaults to False.
2170 Returns:
2171 str: String containing console contents as HTML.
2172 """
2173 assert (
2174 self.record
2175 ), "To export console contents set record=True in the constructor or instance"
2176 fragments: List[str] = []
2177 append = fragments.append
2178 _theme = theme or DEFAULT_TERMINAL_THEME
2179 stylesheet = ""
2181 render_code_format = CONSOLE_HTML_FORMAT if code_format is None else code_format
2183 with self._record_buffer_lock:
2184 if inline_styles:
2185 for text, style, _ in Segment.filter_control(
2186 Segment.simplify(self._record_buffer)
2187 ):
2188 text = escape(text)
2189 if style:
2190 rule = style.get_html_style(_theme)
2191 if style.link:
2192 text = f'<a href="{style.link}">{text}</a>'
2193 text = f'<span style="{rule}">{text}</span>' if rule else text
2194 append(text)
2195 else:
2196 styles: Dict[str, int] = {}
2197 for text, style, _ in Segment.filter_control(
2198 Segment.simplify(self._record_buffer)
2199 ):
2200 text = escape(text)
2201 if style:
2202 rule = style.get_html_style(_theme)
2203 style_number = styles.setdefault(rule, len(styles) + 1)
2204 if style.link:
2205 text = f'<a class="r{style_number}" href="{style.link}">{text}</a>'
2206 else:
2207 text = f'<span class="r{style_number}">{text}</span>'
2208 append(text)
2209 stylesheet_rules: List[str] = []
2210 stylesheet_append = stylesheet_rules.append
2211 for style_rule, style_number in styles.items():
2212 if style_rule:
2213 stylesheet_append(f".r{style_number} {{{style_rule}}}")
2214 stylesheet = "\n".join(stylesheet_rules)
2216 rendered_code = render_code_format.format(
2217 code="".join(fragments),
2218 stylesheet=stylesheet,
2219 foreground=_theme.foreground_color.hex,
2220 background=_theme.background_color.hex,
2221 )
2222 if clear:
2223 del self._record_buffer[:]
2224 return rendered_code
2226 def save_html(
2227 self,
2228 path: str,
2229 *,
2230 theme: Optional[TerminalTheme] = None,
2231 clear: bool = True,
2232 code_format: str = CONSOLE_HTML_FORMAT,
2233 inline_styles: bool = False,
2234 ) -> None:
2235 """Generate HTML from console contents and write to a file (requires record=True argument in constructor).
2237 Args:
2238 path (str): Path to write html file.
2239 theme (TerminalTheme, optional): TerminalTheme object containing console colors.
2240 clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``.
2241 code_format (str, optional): Format string to render HTML. In addition to '{foreground}',
2242 '{background}', and '{code}', should contain '{stylesheet}' if inline_styles is ``False``.
2243 inline_styles (bool, optional): If ``True`` styles will be inlined in to spans, which makes files
2244 larger but easier to cut and paste markup. If ``False``, styles will be embedded in a style tag.
2245 Defaults to False.
2247 """
2248 html = self.export_html(
2249 theme=theme,
2250 clear=clear,
2251 code_format=code_format,
2252 inline_styles=inline_styles,
2253 )
2254 with open(path, "wt", encoding="utf-8") as write_file:
2255 write_file.write(html)
2257 def export_svg(
2258 self,
2259 *,
2260 title: str = "Rich",
2261 theme: Optional[TerminalTheme] = None,
2262 clear: bool = True,
2263 code_format: str = CONSOLE_SVG_FORMAT,
2264 font_aspect_ratio: float = 0.61,
2265 unique_id: Optional[str] = None,
2266 ) -> str:
2267 """
2268 Generate an SVG from the console contents (requires record=True in Console constructor).
2270 Args:
2271 title (str, optional): The title of the tab in the output image
2272 theme (TerminalTheme, optional): The ``TerminalTheme`` object to use to style the terminal
2273 clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``
2274 code_format (str, optional): Format string used to generate the SVG. Rich will inject a number of variables
2275 into the string in order to form the final SVG output. The default template used and the variables
2276 injected by Rich can be found by inspecting the ``console.CONSOLE_SVG_FORMAT`` variable.
2277 font_aspect_ratio (float, optional): The width to height ratio of the font used in the ``code_format``
2278 string. Defaults to 0.61, which is the width to height ratio of Fira Code (the default font).
2279 If you aren't specifying a different font inside ``code_format``, you probably don't need this.
2280 unique_id (str, optional): unique id that is used as the prefix for various elements (CSS styles, node
2281 ids). If not set, this defaults to a computed value based on the recorded content.
2282 """
2284 from rich.cells import cell_len
2286 style_cache: Dict[Style, str] = {}
2288 def get_svg_style(style: Style) -> str:
2289 """Convert a Style to CSS rules for SVG."""
2290 if style in style_cache:
2291 return style_cache[style]
2292 css_rules = []
2293 color = (
2294 _theme.foreground_color
2295 if (style.color is None or style.color.is_default)
2296 else style.color.get_truecolor(_theme)
2297 )
2298 bgcolor = (
2299 _theme.background_color
2300 if (style.bgcolor is None or style.bgcolor.is_default)
2301 else style.bgcolor.get_truecolor(_theme)
2302 )
2303 if style.reverse:
2304 color, bgcolor = bgcolor, color
2305 if style.dim:
2306 color = blend_rgb(color, bgcolor, 0.4)
2307 css_rules.append(f"fill: {color.hex}")
2308 if style.bold:
2309 css_rules.append("font-weight: bold")
2310 if style.italic:
2311 css_rules.append("font-style: italic;")
2312 if style.underline:
2313 css_rules.append("text-decoration: underline;")
2314 if style.strike:
2315 css_rules.append("text-decoration: line-through;")
2317 css = ";".join(css_rules)
2318 style_cache[style] = css
2319 return css
2321 _theme = theme or SVG_EXPORT_THEME
2323 width = self.width
2324 char_height = 20
2325 char_width = char_height * font_aspect_ratio
2326 line_height = char_height * 1.22
2328 margin_top = 1
2329 margin_right = 1
2330 margin_bottom = 1
2331 margin_left = 1
2333 padding_top = 40
2334 padding_right = 8
2335 padding_bottom = 8
2336 padding_left = 8
2338 padding_width = padding_left + padding_right
2339 padding_height = padding_top + padding_bottom
2340 margin_width = margin_left + margin_right
2341 margin_height = margin_top + margin_bottom
2343 text_backgrounds: List[str] = []
2344 text_group: List[str] = []
2345 classes: Dict[str, int] = {}
2346 style_no = 1
2348 def escape_text(text: str) -> str:
2349 """HTML escape text and replace spaces with nbsp."""
2350 return escape(text).replace(" ", " ")
2352 def make_tag(
2353 name: str, content: Optional[str] = None, **attribs: object
2354 ) -> str:
2355 """Make a tag from name, content, and attributes."""
2357 def stringify(value: object) -> str:
2358 if isinstance(value, (float)):
2359 return format(value, "g")
2360 return str(value)
2362 tag_attribs = " ".join(
2363 f'{k.lstrip("_").replace("_", "-")}="{stringify(v)}"'
2364 for k, v in attribs.items()
2365 )
2366 return (
2367 f"<{name} {tag_attribs}>{content}</{name}>"
2368 if content
2369 else f"<{name} {tag_attribs}/>"
2370 )
2372 with self._record_buffer_lock:
2373 segments = list(Segment.filter_control(self._record_buffer))
2374 if clear:
2375 self._record_buffer.clear()
2377 if unique_id is None:
2378 unique_id = "terminal-" + str(
2379 zlib.adler32(
2380 ("".join(repr(segment) for segment in segments)).encode(
2381 "utf-8",
2382 "ignore",
2383 )
2384 + title.encode("utf-8", "ignore")
2385 )
2386 )
2387 y = 0
2388 for y, line in enumerate(Segment.split_and_crop_lines(segments, length=width)):
2389 x = 0
2390 for text, style, _control in line:
2391 style = style or Style()
2392 rules = get_svg_style(style)
2393 if rules not in classes:
2394 classes[rules] = style_no
2395 style_no += 1
2396 class_name = f"r{classes[rules]}"
2398 if style.reverse:
2399 has_background = True
2400 background = (
2401 _theme.foreground_color.hex
2402 if style.color is None
2403 else style.color.get_truecolor(_theme).hex
2404 )
2405 else:
2406 bgcolor = style.bgcolor
2407 has_background = bgcolor is not None and not bgcolor.is_default
2408 background = (
2409 _theme.background_color.hex
2410 if style.bgcolor is None
2411 else style.bgcolor.get_truecolor(_theme).hex
2412 )
2414 text_length = cell_len(text)
2415 if has_background:
2416 text_backgrounds.append(
2417 make_tag(
2418 "rect",
2419 fill=background,
2420 x=x * char_width,
2421 y=y * line_height + 1.5,
2422 width=char_width * text_length,
2423 height=line_height + 0.25,
2424 shape_rendering="crispEdges",
2425 )
2426 )
2428 if text != " " * len(text):
2429 text_group.append(
2430 make_tag(
2431 "text",
2432 escape_text(text),
2433 _class=f"{unique_id}-{class_name}",
2434 x=x * char_width,
2435 y=y * line_height + char_height,
2436 textLength=char_width * len(text),
2437 clip_path=f"url(#{unique_id}-line-{y})",
2438 )
2439 )
2440 x += cell_len(text)
2442 line_offsets = [line_no * line_height + 1.5 for line_no in range(y)]
2443 lines = "\n".join(
2444 f"""<clipPath id="{unique_id}-line-{line_no}">
2445 {make_tag("rect", x=0, y=offset, width=char_width * width, height=line_height + 0.25)}
2446 </clipPath>"""
2447 for line_no, offset in enumerate(line_offsets)
2448 )
2450 styles = "\n".join(
2451 f".{unique_id}-r{rule_no} {{ {css} }}" for css, rule_no in classes.items()
2452 )
2453 backgrounds = "".join(text_backgrounds)
2454 matrix = "".join(text_group)
2456 terminal_width = ceil(width * char_width + padding_width)
2457 terminal_height = (y + 1) * line_height + padding_height
2458 chrome = make_tag(
2459 "rect",
2460 fill=_theme.background_color.hex,
2461 stroke="rgba(255,255,255,0.35)",
2462 stroke_width="1",
2463 x=margin_left,
2464 y=margin_top,
2465 width=terminal_width,
2466 height=terminal_height,
2467 rx=8,
2468 )
2470 title_color = _theme.foreground_color.hex
2471 if title:
2472 chrome += make_tag(
2473 "text",
2474 escape_text(title),
2475 _class=f"{unique_id}-title",
2476 fill=title_color,
2477 text_anchor="middle",
2478 x=terminal_width // 2,
2479 y=margin_top + char_height + 6,
2480 )
2481 chrome += f"""
2482 <g transform="translate(26,22)">
2483 <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
2484 <circle cx="22" cy="0" r="7" fill="#febc2e"/>
2485 <circle cx="44" cy="0" r="7" fill="#28c840"/>
2486 </g>
2487 """
2489 svg = code_format.format(
2490 unique_id=unique_id,
2491 char_width=char_width,
2492 char_height=char_height,
2493 line_height=line_height,
2494 terminal_width=char_width * width - 1,
2495 terminal_height=(y + 1) * line_height - 1,
2496 width=terminal_width + margin_width,
2497 height=terminal_height + margin_height,
2498 terminal_x=margin_left + padding_left,
2499 terminal_y=margin_top + padding_top,
2500 styles=styles,
2501 chrome=chrome,
2502 backgrounds=backgrounds,
2503 matrix=matrix,
2504 lines=lines,
2505 )
2506 return svg
2508 def save_svg(
2509 self,
2510 path: str,
2511 *,
2512 title: str = "Rich",
2513 theme: Optional[TerminalTheme] = None,
2514 clear: bool = True,
2515 code_format: str = CONSOLE_SVG_FORMAT,
2516 font_aspect_ratio: float = 0.61,
2517 unique_id: Optional[str] = None,
2518 ) -> None:
2519 """Generate an SVG file from the console contents (requires record=True in Console constructor).
2521 Args:
2522 path (str): The path to write the SVG to.
2523 title (str, optional): The title of the tab in the output image
2524 theme (TerminalTheme, optional): The ``TerminalTheme`` object to use to style the terminal
2525 clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``
2526 code_format (str, optional): Format string used to generate the SVG. Rich will inject a number of variables
2527 into the string in order to form the final SVG output. The default template used and the variables
2528 injected by Rich can be found by inspecting the ``console.CONSOLE_SVG_FORMAT`` variable.
2529 font_aspect_ratio (float, optional): The width to height ratio of the font used in the ``code_format``
2530 string. Defaults to 0.61, which is the width to height ratio of Fira Code (the default font).
2531 If you aren't specifying a different font inside ``code_format``, you probably don't need this.
2532 unique_id (str, optional): unique id that is used as the prefix for various elements (CSS styles, node
2533 ids). If not set, this defaults to a computed value based on the recorded content.
2534 """
2535 svg = self.export_svg(
2536 title=title,
2537 theme=theme,
2538 clear=clear,
2539 code_format=code_format,
2540 font_aspect_ratio=font_aspect_ratio,
2541 unique_id=unique_id,
2542 )
2543 with open(path, "wt", encoding="utf-8") as write_file:
2544 write_file.write(svg)
2547def _svg_hash(svg_main_code: str) -> str:
2548 """Returns a unique hash for the given SVG main code.
2550 Args:
2551 svg_main_code (str): The content we're going to inject in the SVG envelope.
2553 Returns:
2554 str: a hash of the given content
2555 """
2556 return str(zlib.adler32(svg_main_code.encode()))
2559if __name__ == "__main__": # pragma: no cover
2560 console = Console(record=True)
2562 console.log(
2563 "JSONRPC [i]request[/i]",
2564 5,
2565 1.3,
2566 True,
2567 False,
2568 None,
2569 {
2570 "jsonrpc": "2.0",
2571 "method": "subtract",
2572 "params": {"minuend": 42, "subtrahend": 23},
2573 "id": 3,
2574 },
2575 )
2577 console.log("Hello, World!", "{'a': 1}", repr(console))
2579 console.print(
2580 {
2581 "name": None,
2582 "empty": [],
2583 "quiz": {
2584 "sport": {
2585 "answered": True,
2586 "q1": {
2587 "question": "Which one is correct team name in NBA?",
2588 "options": [
2589 "New York Bulls",
2590 "Los Angeles Kings",
2591 "Golden State Warriors",
2592 "Huston Rocket",
2593 ],
2594 "answer": "Huston Rocket",
2595 },
2596 },
2597 "maths": {
2598 "answered": False,
2599 "q1": {
2600 "question": "5 + 7 = ?",
2601 "options": [10, 11, 12, 13],
2602 "answer": 12,
2603 },
2604 "q2": {
2605 "question": "12 - 8 = ?",
2606 "options": [1, 2, 3, 4],
2607 "answer": 4,
2608 },
2609 },
2610 },
2611 }
2612 )