Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/rich/console.py: 51%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import inspect
2import os
3import sys
4import threading
5import zlib
6from abc import ABC, abstractmethod
7from dataclasses import dataclass, field
8from datetime import datetime
9from functools import wraps
10from getpass import getpass
11from html import escape
12from inspect import isclass
13from itertools import islice
14from math import ceil
15from time import monotonic
16from types import FrameType, ModuleType, TracebackType
17from typing import (
18 IO,
19 TYPE_CHECKING,
20 Any,
21 Callable,
22 Dict,
23 Iterable,
24 List,
25 Mapping,
26 NamedTuple,
27 Optional,
28 TextIO,
29 Tuple,
30 Type,
31 Union,
32 cast,
33)
35from rich._null_file import NULL_FILE
37if sys.version_info >= (3, 8):
38 from typing import Literal, Protocol, runtime_checkable
39else:
40 from typing_extensions import (
41 Literal,
42 Protocol,
43 runtime_checkable,
44 ) # pragma: no cover
46from . import errors, themes
47from ._emoji_replace import _emoji_replace
48from ._export_format import CONSOLE_HTML_FORMAT, CONSOLE_SVG_FORMAT
49from ._fileno import get_fileno
50from ._log_render import FormatTimeCallable, LogRender
51from .align import Align, AlignMethod
52from .color import ColorSystem, blend_rgb
53from .control import Control
54from .emoji import EmojiVariant
55from .highlighter import NullHighlighter, ReprHighlighter
56from .markup import render as render_markup
57from .measure import Measurement, measure_renderables
58from .pager import Pager, SystemPager
59from .pretty import Pretty, is_expandable
60from .protocol import rich_cast
61from .region import Region
62from .scope import render_scope
63from .screen import Screen
64from .segment import Segment
65from .style import Style, StyleType
66from .styled import Styled
67from .terminal_theme import DEFAULT_TERMINAL_THEME, SVG_EXPORT_THEME, TerminalTheme
68from .text import Text, TextType
69from .theme import Theme, ThemeStack
71if TYPE_CHECKING:
72 from ._windows import WindowsConsoleFeatures
73 from .live import Live
74 from .status import Status
76JUPYTER_DEFAULT_COLUMNS = 115
77JUPYTER_DEFAULT_LINES = 100
78WINDOWS = sys.platform == "win32"
80HighlighterType = Callable[[Union[str, "Text"]], "Text"]
81JustifyMethod = Literal["default", "left", "center", "right", "full"]
82OverflowMethod = Literal["fold", "crop", "ellipsis", "ignore"]
85class NoChange:
86 pass
89NO_CHANGE = NoChange()
91try:
92 _STDIN_FILENO = sys.__stdin__.fileno() # type: ignore[union-attr]
93except Exception:
94 _STDIN_FILENO = 0
95try:
96 _STDOUT_FILENO = sys.__stdout__.fileno() # type: ignore[union-attr]
97except Exception:
98 _STDOUT_FILENO = 1
99try:
100 _STDERR_FILENO = sys.__stderr__.fileno() # type: ignore[union-attr]
101except Exception:
102 _STDERR_FILENO = 2
104_STD_STREAMS = (_STDIN_FILENO, _STDOUT_FILENO, _STDERR_FILENO)
105_STD_STREAMS_OUTPUT = (_STDOUT_FILENO, _STDERR_FILENO)
108_TERM_COLORS = {
109 "kitty": ColorSystem.EIGHT_BIT,
110 "256color": ColorSystem.EIGHT_BIT,
111 "16color": ColorSystem.STANDARD,
112}
115class ConsoleDimensions(NamedTuple):
116 """Size of the terminal."""
118 width: int
119 """The width of the console in 'cells'."""
120 height: int
121 """The height of the console in lines."""
124@dataclass
125class ConsoleOptions:
126 """Options for __rich_console__ method."""
128 size: ConsoleDimensions
129 """Size of console."""
130 legacy_windows: bool
131 """legacy_windows: flag for legacy windows."""
132 min_width: int
133 """Minimum width of renderable."""
134 max_width: int
135 """Maximum width of renderable."""
136 is_terminal: bool
137 """True if the target is a terminal, otherwise False."""
138 encoding: str
139 """Encoding of terminal."""
140 max_height: int
141 """Height of container (starts as terminal)"""
142 justify: Optional[JustifyMethod] = None
143 """Justify value override for renderable."""
144 overflow: Optional[OverflowMethod] = None
145 """Overflow value override for renderable."""
146 no_wrap: Optional[bool] = False
147 """Disable wrapping for text."""
148 highlight: Optional[bool] = None
149 """Highlight override for render_str."""
150 markup: Optional[bool] = None
151 """Enable markup when rendering strings."""
152 height: Optional[int] = None
154 @property
155 def ascii_only(self) -> bool:
156 """Check if renderables should use ascii only."""
157 return not self.encoding.startswith("utf")
159 def copy(self) -> "ConsoleOptions":
160 """Return a copy of the options.
162 Returns:
163 ConsoleOptions: a copy of self.
164 """
165 options: ConsoleOptions = ConsoleOptions.__new__(ConsoleOptions)
166 options.__dict__ = self.__dict__.copy()
167 return options
169 def update(
170 self,
171 *,
172 width: Union[int, NoChange] = NO_CHANGE,
173 min_width: Union[int, NoChange] = NO_CHANGE,
174 max_width: Union[int, NoChange] = NO_CHANGE,
175 justify: Union[Optional[JustifyMethod], NoChange] = NO_CHANGE,
176 overflow: Union[Optional[OverflowMethod], NoChange] = NO_CHANGE,
177 no_wrap: Union[Optional[bool], NoChange] = NO_CHANGE,
178 highlight: Union[Optional[bool], NoChange] = NO_CHANGE,
179 markup: Union[Optional[bool], NoChange] = NO_CHANGE,
180 height: Union[Optional[int], NoChange] = NO_CHANGE,
181 ) -> "ConsoleOptions":
182 """Update values, return a copy."""
183 options = self.copy()
184 if not isinstance(width, NoChange):
185 options.min_width = options.max_width = max(0, width)
186 if not isinstance(min_width, NoChange):
187 options.min_width = min_width
188 if not isinstance(max_width, NoChange):
189 options.max_width = max_width
190 if not isinstance(justify, NoChange):
191 options.justify = justify
192 if not isinstance(overflow, NoChange):
193 options.overflow = overflow
194 if not isinstance(no_wrap, NoChange):
195 options.no_wrap = no_wrap
196 if not isinstance(highlight, NoChange):
197 options.highlight = highlight
198 if not isinstance(markup, NoChange):
199 options.markup = markup
200 if not isinstance(height, NoChange):
201 if height is not None:
202 options.max_height = height
203 options.height = None if height is None else max(0, height)
204 return options
206 def update_width(self, width: int) -> "ConsoleOptions":
207 """Update just the width, return a copy.
209 Args:
210 width (int): New width (sets both min_width and max_width)
212 Returns:
213 ~ConsoleOptions: New console options instance.
214 """
215 options = self.copy()
216 options.min_width = options.max_width = max(0, width)
217 return options
219 def update_height(self, height: int) -> "ConsoleOptions":
220 """Update the height, and return a copy.
222 Args:
223 height (int): New height
225 Returns:
226 ~ConsoleOptions: New Console options instance.
227 """
228 options = self.copy()
229 options.max_height = options.height = height
230 return options
232 def reset_height(self) -> "ConsoleOptions":
233 """Return a copy of the options with height set to ``None``.
235 Returns:
236 ~ConsoleOptions: New console options instance.
237 """
238 options = self.copy()
239 options.height = None
240 return options
242 def update_dimensions(self, width: int, height: int) -> "ConsoleOptions":
243 """Update the width and height, and return a copy.
245 Args:
246 width (int): New width (sets both min_width and max_width).
247 height (int): New height.
249 Returns:
250 ~ConsoleOptions: New console options instance.
251 """
252 options = self.copy()
253 options.min_width = options.max_width = max(0, width)
254 options.height = options.max_height = height
255 return options
258@runtime_checkable
259class RichCast(Protocol):
260 """An object that may be 'cast' to a console renderable."""
262 def __rich__(
263 self,
264 ) -> Union["ConsoleRenderable", "RichCast", str]: # pragma: no cover
265 ...
268@runtime_checkable
269class ConsoleRenderable(Protocol):
270 """An object that supports the console protocol."""
272 def __rich_console__(
273 self, console: "Console", options: "ConsoleOptions"
274 ) -> "RenderResult": # pragma: no cover
275 ...
278# A type that may be rendered by Console.
279RenderableType = Union[ConsoleRenderable, RichCast, str]
280"""A string or any object that may be rendered by Rich."""
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
715 self._file = file
716 self.quiet = quiet
717 self.stderr = stderr
719 if color_system is None:
720 self._color_system = None
721 elif color_system == "auto":
722 self._color_system = self._detect_color_system()
723 else:
724 self._color_system = COLOR_SYSTEMS[color_system]
726 self._lock = threading.RLock()
727 self._log_render = LogRender(
728 show_time=log_time,
729 show_path=log_path,
730 time_format=log_time_format,
731 )
732 self.highlighter: HighlighterType = highlighter or _null_highlighter
733 self.safe_box = safe_box
734 self.get_datetime = get_datetime or datetime.now
735 self.get_time = get_time or monotonic
736 self.style = style
737 self.no_color = (
738 no_color
739 if no_color is not None
740 else self._environ.get("NO_COLOR", "") != ""
741 )
742 self.is_interactive = (
743 (self.is_terminal and not self.is_dumb_terminal)
744 if force_interactive is None
745 else force_interactive
746 )
748 self._record_buffer_lock = threading.RLock()
749 self._thread_locals = ConsoleThreadLocals(
750 theme_stack=ThemeStack(themes.DEFAULT if theme is None else theme)
751 )
752 self._record_buffer: List[Segment] = []
753 self._render_hooks: List[RenderHook] = []
754 self._live: Optional["Live"] = None
755 self._is_alt_screen = False
757 def __repr__(self) -> str:
758 return f"<console width={self.width} {self._color_system!s}>"
760 @property
761 def file(self) -> IO[str]:
762 """Get the file object to write to."""
763 file = self._file or (sys.stderr if self.stderr else sys.stdout)
764 file = getattr(file, "rich_proxied_file", file)
765 if file is None:
766 file = NULL_FILE
767 return file
769 @file.setter
770 def file(self, new_file: IO[str]) -> None:
771 """Set a new file object."""
772 self._file = new_file
774 @property
775 def _buffer(self) -> List[Segment]:
776 """Get a thread local buffer."""
777 return self._thread_locals.buffer
779 @property
780 def _buffer_index(self) -> int:
781 """Get a thread local buffer."""
782 return self._thread_locals.buffer_index
784 @_buffer_index.setter
785 def _buffer_index(self, value: int) -> None:
786 self._thread_locals.buffer_index = value
788 @property
789 def _theme_stack(self) -> ThemeStack:
790 """Get the thread local theme stack."""
791 return self._thread_locals.theme_stack
793 def _detect_color_system(self) -> Optional[ColorSystem]:
794 """Detect color system from env vars."""
795 if self.is_jupyter:
796 return ColorSystem.TRUECOLOR
797 if not self.is_terminal or self.is_dumb_terminal:
798 return None
799 if WINDOWS: # pragma: no cover
800 if self.legacy_windows: # pragma: no cover
801 return ColorSystem.WINDOWS
802 windows_console_features = get_windows_console_features()
803 return (
804 ColorSystem.TRUECOLOR
805 if windows_console_features.truecolor
806 else ColorSystem.EIGHT_BIT
807 )
808 else:
809 color_term = self._environ.get("COLORTERM", "").strip().lower()
810 if color_term in ("truecolor", "24bit"):
811 return ColorSystem.TRUECOLOR
812 term = self._environ.get("TERM", "").strip().lower()
813 _term_name, _hyphen, colors = term.rpartition("-")
814 color_system = _TERM_COLORS.get(colors, ColorSystem.STANDARD)
815 return color_system
817 def _enter_buffer(self) -> None:
818 """Enter in to a buffer context, and buffer all output."""
819 self._buffer_index += 1
821 def _exit_buffer(self) -> None:
822 """Leave buffer context, and render content if required."""
823 self._buffer_index -= 1
824 self._check_buffer()
826 def set_live(self, live: "Live") -> None:
827 """Set Live instance. Used by Live context manager.
829 Args:
830 live (Live): Live instance using this Console.
832 Raises:
833 errors.LiveError: If this Console has a Live context currently active.
834 """
835 with self._lock:
836 if self._live is not None:
837 raise errors.LiveError("Only one live display may be active at once")
838 self._live = live
840 def clear_live(self) -> None:
841 """Clear the Live instance."""
842 with self._lock:
843 self._live = None
845 def push_render_hook(self, hook: RenderHook) -> None:
846 """Add a new render hook to the stack.
848 Args:
849 hook (RenderHook): Render hook instance.
850 """
851 with self._lock:
852 self._render_hooks.append(hook)
854 def pop_render_hook(self) -> None:
855 """Pop the last renderhook from the stack."""
856 with self._lock:
857 self._render_hooks.pop()
859 def __enter__(self) -> "Console":
860 """Own context manager to enter buffer context."""
861 self._enter_buffer()
862 return self
864 def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
865 """Exit buffer context."""
866 self._exit_buffer()
868 def begin_capture(self) -> None:
869 """Begin capturing console output. Call :meth:`end_capture` to exit capture mode and return output."""
870 self._enter_buffer()
872 def end_capture(self) -> str:
873 """End capture mode and return captured string.
875 Returns:
876 str: Console output.
877 """
878 render_result = self._render_buffer(self._buffer)
879 del self._buffer[:]
880 self._exit_buffer()
881 return render_result
883 def push_theme(self, theme: Theme, *, inherit: bool = True) -> None:
884 """Push a new theme on to the top of the stack, replacing the styles from the previous theme.
885 Generally speaking, you should call :meth:`~rich.console.Console.use_theme` to get a context manager, rather
886 than calling this method directly.
888 Args:
889 theme (Theme): A theme instance.
890 inherit (bool, optional): Inherit existing styles. Defaults to True.
891 """
892 self._theme_stack.push_theme(theme, inherit=inherit)
894 def pop_theme(self) -> None:
895 """Remove theme from top of stack, restoring previous theme."""
896 self._theme_stack.pop_theme()
898 def use_theme(self, theme: Theme, *, inherit: bool = True) -> ThemeContext:
899 """Use a different theme for the duration of the context manager.
901 Args:
902 theme (Theme): Theme instance to user.
903 inherit (bool, optional): Inherit existing console styles. Defaults to True.
905 Returns:
906 ThemeContext: [description]
907 """
908 return ThemeContext(self, theme, inherit)
910 @property
911 def color_system(self) -> Optional[str]:
912 """Get color system string.
914 Returns:
915 Optional[str]: "standard", "256" or "truecolor".
916 """
918 if self._color_system is not None:
919 return _COLOR_SYSTEMS_NAMES[self._color_system]
920 else:
921 return None
923 @property
924 def encoding(self) -> str:
925 """Get the encoding of the console file, e.g. ``"utf-8"``.
927 Returns:
928 str: A standard encoding string.
929 """
930 return (getattr(self.file, "encoding", "utf-8") or "utf-8").lower()
932 @property
933 def is_terminal(self) -> bool:
934 """Check if the console is writing to a terminal.
936 Returns:
937 bool: True if the console writing to a device capable of
938 understanding escape sequences, otherwise False.
939 """
940 # If dev has explicitly set this value, return it
941 if self._force_terminal is not None:
942 return self._force_terminal
944 # Fudge for Idle
945 if hasattr(sys.stdin, "__module__") and sys.stdin.__module__.startswith(
946 "idlelib"
947 ):
948 # Return False for Idle which claims to be a tty but can't handle ansi codes
949 return False
951 if self.is_jupyter:
952 # return False for Jupyter, which may have FORCE_COLOR set
953 return False
955 environ = self._environ
957 tty_compatible = environ.get("TTY_COMPATIBLE", "")
958 # 0 indicates device is not tty compatible
959 if tty_compatible == "0":
960 return False
961 # 1 indicates device is tty compatible
962 if tty_compatible == "1":
963 return True
965 # https://force-color.org/
966 force_color = environ.get("FORCE_COLOR")
967 if force_color is not None:
968 return force_color != ""
970 # Any other value defaults to auto detect
971 isatty: Optional[Callable[[], bool]] = getattr(self.file, "isatty", None)
972 try:
973 return False if isatty is None else isatty()
974 except ValueError:
975 # in some situation (at the end of a pytest run for example) isatty() can raise
976 # ValueError: I/O operation on closed file
977 # return False because we aren't in a terminal anymore
978 return False
980 @property
981 def is_dumb_terminal(self) -> bool:
982 """Detect dumb terminal.
984 Returns:
985 bool: True if writing to a dumb terminal, otherwise False.
987 """
988 _term = self._environ.get("TERM", "")
989 is_dumb = _term.lower() in ("dumb", "unknown")
990 return self.is_terminal and is_dumb
992 @property
993 def options(self) -> ConsoleOptions:
994 """Get default console options."""
995 return ConsoleOptions(
996 max_height=self.size.height,
997 size=self.size,
998 legacy_windows=self.legacy_windows,
999 min_width=1,
1000 max_width=self.width,
1001 encoding=self.encoding,
1002 is_terminal=self.is_terminal,
1003 )
1005 @property
1006 def size(self) -> ConsoleDimensions:
1007 """Get the size of the console.
1009 Returns:
1010 ConsoleDimensions: A named tuple containing the dimensions.
1011 """
1013 if self._width is not None and self._height is not None:
1014 return ConsoleDimensions(self._width - self.legacy_windows, self._height)
1016 if self.is_dumb_terminal:
1017 return ConsoleDimensions(80, 25)
1019 width: Optional[int] = None
1020 height: Optional[int] = None
1022 streams = _STD_STREAMS_OUTPUT if WINDOWS else _STD_STREAMS
1023 for file_descriptor in streams:
1024 try:
1025 width, height = os.get_terminal_size(file_descriptor)
1026 except (AttributeError, ValueError, OSError): # Probably not a terminal
1027 pass
1028 else:
1029 break
1031 columns = self._environ.get("COLUMNS")
1032 if columns is not None and columns.isdigit():
1033 width = int(columns)
1034 lines = self._environ.get("LINES")
1035 if lines is not None and lines.isdigit():
1036 height = int(lines)
1038 # get_terminal_size can report 0, 0 if run from pseudo-terminal
1039 width = width or 80
1040 height = height or 25
1041 return ConsoleDimensions(
1042 width - self.legacy_windows if self._width is None else self._width,
1043 height if self._height is None else self._height,
1044 )
1046 @size.setter
1047 def size(self, new_size: Tuple[int, int]) -> None:
1048 """Set a new size for the terminal.
1050 Args:
1051 new_size (Tuple[int, int]): New width and height.
1052 """
1053 width, height = new_size
1054 self._width = width
1055 self._height = height
1057 @property
1058 def width(self) -> int:
1059 """Get the width of the console.
1061 Returns:
1062 int: The width (in characters) of the console.
1063 """
1064 return self.size.width
1066 @width.setter
1067 def width(self, width: int) -> None:
1068 """Set width.
1070 Args:
1071 width (int): New width.
1072 """
1073 self._width = width
1075 @property
1076 def height(self) -> int:
1077 """Get the height of the console.
1079 Returns:
1080 int: The height (in lines) of the console.
1081 """
1082 return self.size.height
1084 @height.setter
1085 def height(self, height: int) -> None:
1086 """Set height.
1088 Args:
1089 height (int): new height.
1090 """
1091 self._height = height
1093 def bell(self) -> None:
1094 """Play a 'bell' sound (if supported by the terminal)."""
1095 self.control(Control.bell())
1097 def capture(self) -> Capture:
1098 """A context manager to *capture* the result of print() or log() in a string,
1099 rather than writing it to the console.
1101 Example:
1102 >>> from rich.console import Console
1103 >>> console = Console()
1104 >>> with console.capture() as capture:
1105 ... console.print("[bold magenta]Hello World[/]")
1106 >>> print(capture.get())
1108 Returns:
1109 Capture: Context manager with disables writing to the terminal.
1110 """
1111 capture = Capture(self)
1112 return capture
1114 def pager(
1115 self, pager: Optional[Pager] = None, styles: bool = False, links: bool = False
1116 ) -> PagerContext:
1117 """A context manager to display anything printed within a "pager". The pager application
1118 is defined by the system and will typically support at least pressing a key to scroll.
1120 Args:
1121 pager (Pager, optional): A pager object, or None to use :class:`~rich.pager.SystemPager`. Defaults to None.
1122 styles (bool, optional): Show styles in pager. Defaults to False.
1123 links (bool, optional): Show links in pager. Defaults to False.
1125 Example:
1126 >>> from rich.console import Console
1127 >>> from rich.__main__ import make_test_card
1128 >>> console = Console()
1129 >>> with console.pager():
1130 console.print(make_test_card())
1132 Returns:
1133 PagerContext: A context manager.
1134 """
1135 return PagerContext(self, pager=pager, styles=styles, links=links)
1137 def line(self, count: int = 1) -> None:
1138 """Write new line(s).
1140 Args:
1141 count (int, optional): Number of new lines. Defaults to 1.
1142 """
1144 assert count >= 0, "count must be >= 0"
1145 self.print(NewLine(count))
1147 def clear(self, home: bool = True) -> None:
1148 """Clear the screen.
1150 Args:
1151 home (bool, optional): Also move the cursor to 'home' position. Defaults to True.
1152 """
1153 if home:
1154 self.control(Control.clear(), Control.home())
1155 else:
1156 self.control(Control.clear())
1158 def status(
1159 self,
1160 status: RenderableType,
1161 *,
1162 spinner: str = "dots",
1163 spinner_style: StyleType = "status.spinner",
1164 speed: float = 1.0,
1165 refresh_per_second: float = 12.5,
1166 ) -> "Status":
1167 """Display a status and spinner.
1169 Args:
1170 status (RenderableType): A status renderable (str or Text typically).
1171 spinner (str, optional): Name of spinner animation (see python -m rich.spinner). Defaults to "dots".
1172 spinner_style (StyleType, optional): Style of spinner. Defaults to "status.spinner".
1173 speed (float, optional): Speed factor for spinner animation. Defaults to 1.0.
1174 refresh_per_second (float, optional): Number of refreshes per second. Defaults to 12.5.
1176 Returns:
1177 Status: A Status object that may be used as a context manager.
1178 """
1179 from .status import Status
1181 status_renderable = Status(
1182 status,
1183 console=self,
1184 spinner=spinner,
1185 spinner_style=spinner_style,
1186 speed=speed,
1187 refresh_per_second=refresh_per_second,
1188 )
1189 return status_renderable
1191 def show_cursor(self, show: bool = True) -> bool:
1192 """Show or hide the cursor.
1194 Args:
1195 show (bool, optional): Set visibility of the cursor.
1196 """
1197 if self.is_terminal:
1198 self.control(Control.show_cursor(show))
1199 return True
1200 return False
1202 def set_alt_screen(self, enable: bool = True) -> bool:
1203 """Enables alternative screen mode.
1205 Note, if you enable this mode, you should ensure that is disabled before
1206 the application exits. See :meth:`~rich.Console.screen` for a context manager
1207 that handles this for you.
1209 Args:
1210 enable (bool, optional): Enable (True) or disable (False) alternate screen. Defaults to True.
1212 Returns:
1213 bool: True if the control codes were written.
1215 """
1216 changed = False
1217 if self.is_terminal and not self.legacy_windows:
1218 self.control(Control.alt_screen(enable))
1219 changed = True
1220 self._is_alt_screen = enable
1221 return changed
1223 @property
1224 def is_alt_screen(self) -> bool:
1225 """Check if the alt screen was enabled.
1227 Returns:
1228 bool: True if the alt screen was enabled, otherwise False.
1229 """
1230 return self._is_alt_screen
1232 def set_window_title(self, title: str) -> bool:
1233 """Set the title of the console terminal window.
1235 Warning: There is no means within Rich of "resetting" the window title to its
1236 previous value, meaning the title you set will persist even after your application
1237 exits.
1239 ``fish`` shell resets the window title before and after each command by default,
1240 negating this issue. Windows Terminal and command prompt will also reset the title for you.
1241 Most other shells and terminals, however, do not do this.
1243 Some terminals may require configuration changes before you can set the title.
1244 Some terminals may not support setting the title at all.
1246 Other software (including the terminal itself, the shell, custom prompts, plugins, etc.)
1247 may also set the terminal window title. This could result in whatever value you write
1248 using this method being overwritten.
1250 Args:
1251 title (str): The new title of the terminal window.
1253 Returns:
1254 bool: True if the control code to change the terminal title was
1255 written, otherwise False. Note that a return value of True
1256 does not guarantee that the window title has actually changed,
1257 since the feature may be unsupported/disabled in some terminals.
1258 """
1259 if self.is_terminal:
1260 self.control(Control.title(title))
1261 return True
1262 return False
1264 def screen(
1265 self, hide_cursor: bool = True, style: Optional[StyleType] = None
1266 ) -> "ScreenContext":
1267 """Context manager to enable and disable 'alternative screen' mode.
1269 Args:
1270 hide_cursor (bool, optional): Also hide the cursor. Defaults to False.
1271 style (Style, optional): Optional style for screen. Defaults to None.
1273 Returns:
1274 ~ScreenContext: Context which enables alternate screen on enter, and disables it on exit.
1275 """
1276 return ScreenContext(self, hide_cursor=hide_cursor, style=style or "")
1278 def measure(
1279 self, renderable: RenderableType, *, options: Optional[ConsoleOptions] = None
1280 ) -> Measurement:
1281 """Measure a renderable. Returns a :class:`~rich.measure.Measurement` object which contains
1282 information regarding the number of characters required to print the renderable.
1284 Args:
1285 renderable (RenderableType): Any renderable or string.
1286 options (Optional[ConsoleOptions], optional): Options to use when measuring, or None
1287 to use default options. Defaults to None.
1289 Returns:
1290 Measurement: A measurement of the renderable.
1291 """
1292 measurement = Measurement.get(self, options or self.options, renderable)
1293 return measurement
1295 def render(
1296 self, renderable: RenderableType, options: Optional[ConsoleOptions] = None
1297 ) -> Iterable[Segment]:
1298 """Render an object in to an iterable of `Segment` instances.
1300 This method contains the logic for rendering objects with the console protocol.
1301 You are unlikely to need to use it directly, unless you are extending the library.
1303 Args:
1304 renderable (RenderableType): An object supporting the console protocol, or
1305 an object that may be converted to a string.
1306 options (ConsoleOptions, optional): An options object, or None to use self.options. Defaults to None.
1308 Returns:
1309 Iterable[Segment]: An iterable of segments that may be rendered.
1310 """
1312 _options = options or self.options
1313 if _options.max_width < 1:
1314 # No space to render anything. This prevents potential recursion errors.
1315 return
1316 render_iterable: RenderResult
1318 renderable = rich_cast(renderable)
1319 if hasattr(renderable, "__rich_console__") and not isclass(renderable):
1320 render_iterable = renderable.__rich_console__(self, _options)
1321 elif isinstance(renderable, str):
1322 text_renderable = self.render_str(
1323 renderable, highlight=_options.highlight, markup=_options.markup
1324 )
1325 render_iterable = text_renderable.__rich_console__(self, _options)
1326 else:
1327 raise errors.NotRenderableError(
1328 f"Unable to render {renderable!r}; "
1329 "A str, Segment or object with __rich_console__ method is required"
1330 )
1332 try:
1333 iter_render = iter(render_iterable)
1334 except TypeError:
1335 raise errors.NotRenderableError(
1336 f"object {render_iterable!r} is not renderable"
1337 )
1338 _Segment = Segment
1339 _options = _options.reset_height()
1340 for render_output in iter_render:
1341 if isinstance(render_output, _Segment):
1342 yield render_output
1343 else:
1344 yield from self.render(render_output, _options)
1346 def render_lines(
1347 self,
1348 renderable: RenderableType,
1349 options: Optional[ConsoleOptions] = None,
1350 *,
1351 style: Optional[Style] = None,
1352 pad: bool = True,
1353 new_lines: bool = False,
1354 ) -> List[List[Segment]]:
1355 """Render objects in to a list of lines.
1357 The output of render_lines is useful when further formatting of rendered console text
1358 is required, such as the Panel class which draws a border around any renderable object.
1360 Args:
1361 renderable (RenderableType): Any object renderable in the console.
1362 options (Optional[ConsoleOptions], optional): Console options, or None to use self.options. Default to ``None``.
1363 style (Style, optional): Optional style to apply to renderables. Defaults to ``None``.
1364 pad (bool, optional): Pad lines shorter than render width. Defaults to ``True``.
1365 new_lines (bool, optional): Include "\n" characters at end of lines.
1367 Returns:
1368 List[List[Segment]]: A list of lines, where a line is a list of Segment objects.
1369 """
1370 with self._lock:
1371 render_options = options or self.options
1372 _rendered = self.render(renderable, render_options)
1373 if style:
1374 _rendered = Segment.apply_style(_rendered, style)
1376 render_height = render_options.height
1377 if render_height is not None:
1378 render_height = max(0, render_height)
1380 lines = list(
1381 islice(
1382 Segment.split_and_crop_lines(
1383 _rendered,
1384 render_options.max_width,
1385 include_new_lines=new_lines,
1386 pad=pad,
1387 style=style,
1388 ),
1389 None,
1390 render_height,
1391 )
1392 )
1393 if render_options.height is not None:
1394 extra_lines = render_options.height - len(lines)
1395 if extra_lines > 0:
1396 pad_line = [
1397 (
1398 [
1399 Segment(" " * render_options.max_width, style),
1400 Segment("\n"),
1401 ]
1402 if new_lines
1403 else [Segment(" " * render_options.max_width, style)]
1404 )
1405 ]
1406 lines.extend(pad_line * extra_lines)
1408 return lines
1410 def render_str(
1411 self,
1412 text: str,
1413 *,
1414 style: Union[str, Style] = "",
1415 justify: Optional[JustifyMethod] = None,
1416 overflow: Optional[OverflowMethod] = None,
1417 emoji: Optional[bool] = None,
1418 markup: Optional[bool] = None,
1419 highlight: Optional[bool] = None,
1420 highlighter: Optional[HighlighterType] = None,
1421 ) -> "Text":
1422 """Convert a string to a Text instance. This is called automatically if
1423 you print or log a string.
1425 Args:
1426 text (str): Text to render.
1427 style (Union[str, Style], optional): Style to apply to rendered text.
1428 justify (str, optional): Justify method: "default", "left", "center", "full", or "right". Defaults to ``None``.
1429 overflow (str, optional): Overflow method: "crop", "fold", or "ellipsis". Defaults to ``None``.
1430 emoji (Optional[bool], optional): Enable emoji, or ``None`` to use Console default.
1431 markup (Optional[bool], optional): Enable markup, or ``None`` to use Console default.
1432 highlight (Optional[bool], optional): Enable highlighting, or ``None`` to use Console default.
1433 highlighter (HighlighterType, optional): Optional highlighter to apply.
1434 Returns:
1435 ConsoleRenderable: Renderable object.
1437 """
1438 emoji_enabled = emoji or (emoji is None and self._emoji)
1439 markup_enabled = markup or (markup is None and self._markup)
1440 highlight_enabled = highlight or (highlight is None and self._highlight)
1442 if markup_enabled:
1443 rich_text = render_markup(
1444 text,
1445 style=style,
1446 emoji=emoji_enabled,
1447 emoji_variant=self._emoji_variant,
1448 )
1449 rich_text.justify = justify
1450 rich_text.overflow = overflow
1451 else:
1452 rich_text = Text(
1453 (
1454 _emoji_replace(text, default_variant=self._emoji_variant)
1455 if emoji_enabled
1456 else text
1457 ),
1458 justify=justify,
1459 overflow=overflow,
1460 style=style,
1461 )
1463 _highlighter = (highlighter or self.highlighter) if highlight_enabled else None
1464 if _highlighter is not None:
1465 highlight_text = _highlighter(str(rich_text))
1466 highlight_text.copy_styles(rich_text)
1467 return highlight_text
1469 return rich_text
1471 def get_style(
1472 self, name: Union[str, Style], *, default: Optional[Union[Style, str]] = None
1473 ) -> Style:
1474 """Get a Style instance by its theme name or parse a definition.
1476 Args:
1477 name (str): The name of a style or a style definition.
1479 Returns:
1480 Style: A Style object.
1482 Raises:
1483 MissingStyle: If no style could be parsed from name.
1485 """
1486 if isinstance(name, Style):
1487 return name
1489 try:
1490 style = self._theme_stack.get(name)
1491 if style is None:
1492 style = Style.parse(name)
1493 return style.copy() if style.link else style
1494 except errors.StyleSyntaxError as error:
1495 if default is not None:
1496 return self.get_style(default)
1497 raise errors.MissingStyle(
1498 f"Failed to get style {name!r}; {error}"
1499 ) from None
1501 def _collect_renderables(
1502 self,
1503 objects: Iterable[Any],
1504 sep: str,
1505 end: str,
1506 *,
1507 justify: Optional[JustifyMethod] = None,
1508 emoji: Optional[bool] = None,
1509 markup: Optional[bool] = None,
1510 highlight: Optional[bool] = None,
1511 ) -> List[ConsoleRenderable]:
1512 """Combine a number of renderables and text into one renderable.
1514 Args:
1515 objects (Iterable[Any]): Anything that Rich can render.
1516 sep (str): String to write between print data.
1517 end (str): String to write at end of print data.
1518 justify (str, optional): One of "left", "right", "center", or "full". Defaults to ``None``.
1519 emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default.
1520 markup (Optional[bool], optional): Enable markup, or ``None`` to use console default.
1521 highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use console default.
1523 Returns:
1524 List[ConsoleRenderable]: A list of things to render.
1525 """
1526 renderables: List[ConsoleRenderable] = []
1527 _append = renderables.append
1528 text: List[Text] = []
1529 append_text = text.append
1531 append = _append
1532 if justify in ("left", "center", "right"):
1534 def align_append(renderable: RenderableType) -> None:
1535 _append(Align(renderable, cast(AlignMethod, justify)))
1537 append = align_append
1539 _highlighter: HighlighterType = _null_highlighter
1540 if highlight or (highlight is None and self._highlight):
1541 _highlighter = self.highlighter
1543 def check_text() -> None:
1544 if text:
1545 sep_text = Text(sep, justify=justify, end=end)
1546 append(sep_text.join(text))
1547 text.clear()
1549 for renderable in objects:
1550 renderable = rich_cast(renderable)
1551 if isinstance(renderable, str):
1552 append_text(
1553 self.render_str(
1554 renderable,
1555 emoji=emoji,
1556 markup=markup,
1557 highlight=highlight,
1558 highlighter=_highlighter,
1559 )
1560 )
1561 elif isinstance(renderable, Text):
1562 append_text(renderable)
1563 elif isinstance(renderable, ConsoleRenderable):
1564 check_text()
1565 append(renderable)
1566 elif is_expandable(renderable):
1567 check_text()
1568 append(Pretty(renderable, highlighter=_highlighter))
1569 else:
1570 append_text(_highlighter(str(renderable)))
1572 check_text()
1574 if self.style is not None:
1575 style = self.get_style(self.style)
1576 renderables = [Styled(renderable, style) for renderable in renderables]
1578 return renderables
1580 def rule(
1581 self,
1582 title: TextType = "",
1583 *,
1584 characters: str = "─",
1585 style: Union[str, Style] = "rule.line",
1586 align: AlignMethod = "center",
1587 ) -> None:
1588 """Draw a line with optional centered title.
1590 Args:
1591 title (str, optional): Text to render over the rule. Defaults to "".
1592 characters (str, optional): Character(s) to form the line. Defaults to "─".
1593 style (str, optional): Style of line. Defaults to "rule.line".
1594 align (str, optional): How to align the title, one of "left", "center", or "right". Defaults to "center".
1595 """
1596 from .rule import Rule
1598 rule = Rule(title=title, characters=characters, style=style, align=align)
1599 self.print(rule)
1601 def control(self, *control: Control) -> None:
1602 """Insert non-printing control codes.
1604 Args:
1605 control_codes (str): Control codes, such as those that may move the cursor.
1606 """
1607 if not self.is_dumb_terminal:
1608 with self:
1609 self._buffer.extend(_control.segment for _control in control)
1611 def out(
1612 self,
1613 *objects: Any,
1614 sep: str = " ",
1615 end: str = "\n",
1616 style: Optional[Union[str, Style]] = None,
1617 highlight: Optional[bool] = None,
1618 ) -> None:
1619 """Output to the terminal. This is a low-level way of writing to the terminal which unlike
1620 :meth:`~rich.console.Console.print` won't pretty print, wrap text, or apply markup, but will
1621 optionally apply highlighting and a basic style.
1623 Args:
1624 sep (str, optional): String to write between print data. Defaults to " ".
1625 end (str, optional): String to write at end of print data. Defaults to "\\\\n".
1626 style (Union[str, Style], optional): A style to apply to output. Defaults to None.
1627 highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use
1628 console default. Defaults to ``None``.
1629 """
1630 raw_output: str = sep.join(str(_object) for _object in objects)
1631 self.print(
1632 raw_output,
1633 style=style,
1634 highlight=highlight,
1635 emoji=False,
1636 markup=False,
1637 no_wrap=True,
1638 overflow="ignore",
1639 crop=False,
1640 end=end,
1641 )
1643 def print(
1644 self,
1645 *objects: Any,
1646 sep: str = " ",
1647 end: str = "\n",
1648 style: Optional[Union[str, Style]] = None,
1649 justify: Optional[JustifyMethod] = None,
1650 overflow: Optional[OverflowMethod] = None,
1651 no_wrap: Optional[bool] = None,
1652 emoji: Optional[bool] = None,
1653 markup: Optional[bool] = None,
1654 highlight: Optional[bool] = None,
1655 width: Optional[int] = None,
1656 height: Optional[int] = None,
1657 crop: bool = True,
1658 soft_wrap: Optional[bool] = None,
1659 new_line_start: bool = False,
1660 ) -> None:
1661 """Print to the console.
1663 Args:
1664 objects (positional args): Objects to log to the terminal.
1665 sep (str, optional): String to write between print data. Defaults to " ".
1666 end (str, optional): String to write at end of print data. Defaults to "\\\\n".
1667 style (Union[str, Style], optional): A style to apply to output. Defaults to None.
1668 justify (str, optional): Justify method: "default", "left", "right", "center", or "full". Defaults to ``None``.
1669 overflow (str, optional): Overflow method: "ignore", "crop", "fold", or "ellipsis". Defaults to None.
1670 no_wrap (Optional[bool], optional): Disable word wrapping. Defaults to None.
1671 emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default. Defaults to ``None``.
1672 markup (Optional[bool], optional): Enable markup, or ``None`` to use console default. Defaults to ``None``.
1673 highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use console default. Defaults to ``None``.
1674 width (Optional[int], optional): Width of output, or ``None`` to auto-detect. Defaults to ``None``.
1675 crop (Optional[bool], optional): Crop output to width of terminal. Defaults to True.
1676 soft_wrap (bool, optional): Enable soft wrap mode which disables word wrapping and cropping of text or ``None`` for
1677 Console default. Defaults to ``None``.
1678 new_line_start (bool, False): Insert a new line at the start if the output contains more than one line. Defaults to ``False``.
1679 """
1680 if not objects:
1681 objects = (NewLine(),)
1683 if soft_wrap is None:
1684 soft_wrap = self.soft_wrap
1685 if soft_wrap:
1686 if no_wrap is None:
1687 no_wrap = True
1688 if overflow is None:
1689 overflow = "ignore"
1690 crop = False
1691 render_hooks = self._render_hooks[:]
1692 with self:
1693 renderables = self._collect_renderables(
1694 objects,
1695 sep,
1696 end,
1697 justify=justify,
1698 emoji=emoji,
1699 markup=markup,
1700 highlight=highlight,
1701 )
1702 for hook in render_hooks:
1703 renderables = hook.process_renderables(renderables)
1704 render_options = self.options.update(
1705 justify=justify,
1706 overflow=overflow,
1707 width=min(width, self.width) if width is not None else NO_CHANGE,
1708 height=height,
1709 no_wrap=no_wrap,
1710 markup=markup,
1711 highlight=highlight,
1712 )
1714 new_segments: List[Segment] = []
1715 extend = new_segments.extend
1716 render = self.render
1717 if style is None:
1718 for renderable in renderables:
1719 extend(render(renderable, render_options))
1720 else:
1721 for renderable in renderables:
1722 extend(
1723 Segment.apply_style(
1724 render(renderable, render_options), self.get_style(style)
1725 )
1726 )
1727 if new_line_start:
1728 if (
1729 len("".join(segment.text for segment in new_segments).splitlines())
1730 > 1
1731 ):
1732 new_segments.insert(0, Segment.line())
1733 if crop:
1734 buffer_extend = self._buffer.extend
1735 for line in Segment.split_and_crop_lines(
1736 new_segments, self.width, pad=False
1737 ):
1738 buffer_extend(line)
1739 else:
1740 self._buffer.extend(new_segments)
1742 def print_json(
1743 self,
1744 json: Optional[str] = None,
1745 *,
1746 data: Any = None,
1747 indent: Union[None, int, str] = 2,
1748 highlight: bool = True,
1749 skip_keys: bool = False,
1750 ensure_ascii: bool = False,
1751 check_circular: bool = True,
1752 allow_nan: bool = True,
1753 default: Optional[Callable[[Any], Any]] = None,
1754 sort_keys: bool = False,
1755 ) -> None:
1756 """Pretty prints JSON. Output will be valid JSON.
1758 Args:
1759 json (Optional[str]): A string containing JSON.
1760 data (Any): If json is not supplied, then encode this data.
1761 indent (Union[None, int, str], optional): Number of spaces to indent. Defaults to 2.
1762 highlight (bool, optional): Enable highlighting of output: Defaults to True.
1763 skip_keys (bool, optional): Skip keys not of a basic type. Defaults to False.
1764 ensure_ascii (bool, optional): Escape all non-ascii characters. Defaults to False.
1765 check_circular (bool, optional): Check for circular references. Defaults to True.
1766 allow_nan (bool, optional): Allow NaN and Infinity values. Defaults to True.
1767 default (Callable, optional): A callable that converts values that can not be encoded
1768 in to something that can be JSON encoded. Defaults to None.
1769 sort_keys (bool, optional): Sort dictionary keys. Defaults to False.
1770 """
1771 from rich.json import JSON
1773 if json is None:
1774 json_renderable = JSON.from_data(
1775 data,
1776 indent=indent,
1777 highlight=highlight,
1778 skip_keys=skip_keys,
1779 ensure_ascii=ensure_ascii,
1780 check_circular=check_circular,
1781 allow_nan=allow_nan,
1782 default=default,
1783 sort_keys=sort_keys,
1784 )
1785 else:
1786 if not isinstance(json, str):
1787 raise TypeError(
1788 f"json must be str. Did you mean print_json(data={json!r}) ?"
1789 )
1790 json_renderable = JSON(
1791 json,
1792 indent=indent,
1793 highlight=highlight,
1794 skip_keys=skip_keys,
1795 ensure_ascii=ensure_ascii,
1796 check_circular=check_circular,
1797 allow_nan=allow_nan,
1798 default=default,
1799 sort_keys=sort_keys,
1800 )
1801 self.print(json_renderable, soft_wrap=True)
1803 def update_screen(
1804 self,
1805 renderable: RenderableType,
1806 *,
1807 region: Optional[Region] = None,
1808 options: Optional[ConsoleOptions] = None,
1809 ) -> None:
1810 """Update the screen at a given offset.
1812 Args:
1813 renderable (RenderableType): A Rich renderable.
1814 region (Region, optional): Region of screen to update, or None for entire screen. Defaults to None.
1815 x (int, optional): x offset. Defaults to 0.
1816 y (int, optional): y offset. Defaults to 0.
1818 Raises:
1819 errors.NoAltScreen: If the Console isn't in alt screen mode.
1821 """
1822 if not self.is_alt_screen:
1823 raise errors.NoAltScreen("Alt screen must be enabled to call update_screen")
1824 render_options = options or self.options
1825 if region is None:
1826 x = y = 0
1827 render_options = render_options.update_dimensions(
1828 render_options.max_width, render_options.height or self.height
1829 )
1830 else:
1831 x, y, width, height = region
1832 render_options = render_options.update_dimensions(width, height)
1834 lines = self.render_lines(renderable, options=render_options)
1835 self.update_screen_lines(lines, x, y)
1837 def update_screen_lines(
1838 self, lines: List[List[Segment]], x: int = 0, y: int = 0
1839 ) -> None:
1840 """Update lines of the screen at a given offset.
1842 Args:
1843 lines (List[List[Segment]]): Rendered lines (as produced by :meth:`~rich.Console.render_lines`).
1844 x (int, optional): x offset (column no). Defaults to 0.
1845 y (int, optional): y offset (column no). Defaults to 0.
1847 Raises:
1848 errors.NoAltScreen: If the Console isn't in alt screen mode.
1849 """
1850 if not self.is_alt_screen:
1851 raise errors.NoAltScreen("Alt screen must be enabled to call update_screen")
1852 screen_update = ScreenUpdate(lines, x, y)
1853 segments = self.render(screen_update)
1854 self._buffer.extend(segments)
1855 self._check_buffer()
1857 def print_exception(
1858 self,
1859 *,
1860 width: Optional[int] = 100,
1861 extra_lines: int = 3,
1862 theme: Optional[str] = None,
1863 word_wrap: bool = False,
1864 show_locals: bool = False,
1865 suppress: Iterable[Union[str, ModuleType]] = (),
1866 max_frames: int = 100,
1867 ) -> None:
1868 """Prints a rich render of the last exception and traceback.
1870 Args:
1871 width (Optional[int], optional): Number of characters used to render code. Defaults to 100.
1872 extra_lines (int, optional): Additional lines of code to render. Defaults to 3.
1873 theme (str, optional): Override pygments theme used in traceback
1874 word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False.
1875 show_locals (bool, optional): Enable display of local variables. Defaults to False.
1876 suppress (Iterable[Union[str, ModuleType]]): Optional sequence of modules or paths to exclude from traceback.
1877 max_frames (int): Maximum number of frames to show in a traceback, 0 for no maximum. Defaults to 100.
1878 """
1879 from .traceback import Traceback
1881 traceback = Traceback(
1882 width=width,
1883 extra_lines=extra_lines,
1884 theme=theme,
1885 word_wrap=word_wrap,
1886 show_locals=show_locals,
1887 suppress=suppress,
1888 max_frames=max_frames,
1889 )
1890 self.print(traceback)
1892 @staticmethod
1893 def _caller_frame_info(
1894 offset: int,
1895 currentframe: Callable[[], Optional[FrameType]] = inspect.currentframe,
1896 ) -> Tuple[str, int, Dict[str, Any]]:
1897 """Get caller frame information.
1899 Args:
1900 offset (int): the caller offset within the current frame stack.
1901 currentframe (Callable[[], Optional[FrameType]], optional): the callable to use to
1902 retrieve the current frame. Defaults to ``inspect.currentframe``.
1904 Returns:
1905 Tuple[str, int, Dict[str, Any]]: A tuple containing the filename, the line number and
1906 the dictionary of local variables associated with the caller frame.
1908 Raises:
1909 RuntimeError: If the stack offset is invalid.
1910 """
1911 # Ignore the frame of this local helper
1912 offset += 1
1914 frame = currentframe()
1915 if frame is not None:
1916 # Use the faster currentframe where implemented
1917 while offset and frame is not None:
1918 frame = frame.f_back
1919 offset -= 1
1920 assert frame is not None
1921 return frame.f_code.co_filename, frame.f_lineno, frame.f_locals
1922 else:
1923 # Fallback to the slower stack
1924 frame_info = inspect.stack()[offset]
1925 return frame_info.filename, frame_info.lineno, frame_info.frame.f_locals
1927 def log(
1928 self,
1929 *objects: Any,
1930 sep: str = " ",
1931 end: str = "\n",
1932 style: Optional[Union[str, Style]] = None,
1933 justify: Optional[JustifyMethod] = None,
1934 emoji: Optional[bool] = None,
1935 markup: Optional[bool] = None,
1936 highlight: Optional[bool] = None,
1937 log_locals: bool = False,
1938 _stack_offset: int = 1,
1939 ) -> None:
1940 """Log rich content to the terminal.
1942 Args:
1943 objects (positional args): Objects to log to the terminal.
1944 sep (str, optional): String to write between print data. Defaults to " ".
1945 end (str, optional): String to write at end of print data. Defaults to "\\\\n".
1946 style (Union[str, Style], optional): A style to apply to output. Defaults to None.
1947 justify (str, optional): One of "left", "right", "center", or "full". Defaults to ``None``.
1948 emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default. Defaults to None.
1949 markup (Optional[bool], optional): Enable markup, or ``None`` to use console default. Defaults to None.
1950 highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use console default. Defaults to None.
1951 log_locals (bool, optional): Boolean to enable logging of locals where ``log()``
1952 was called. Defaults to False.
1953 _stack_offset (int, optional): Offset of caller from end of call stack. Defaults to 1.
1954 """
1955 if not objects:
1956 objects = (NewLine(),)
1958 render_hooks = self._render_hooks[:]
1960 with self:
1961 renderables = self._collect_renderables(
1962 objects,
1963 sep,
1964 end,
1965 justify=justify,
1966 emoji=emoji,
1967 markup=markup,
1968 highlight=highlight,
1969 )
1970 if style is not None:
1971 renderables = [Styled(renderable, style) for renderable in renderables]
1973 filename, line_no, locals = self._caller_frame_info(_stack_offset)
1974 link_path = None if filename.startswith("<") else os.path.abspath(filename)
1975 path = filename.rpartition(os.sep)[-1]
1976 if log_locals:
1977 locals_map = {
1978 key: value
1979 for key, value in locals.items()
1980 if not key.startswith("__")
1981 }
1982 renderables.append(render_scope(locals_map, title="[i]locals"))
1984 renderables = [
1985 self._log_render(
1986 self,
1987 renderables,
1988 log_time=self.get_datetime(),
1989 path=path,
1990 line_no=line_no,
1991 link_path=link_path,
1992 )
1993 ]
1994 for hook in render_hooks:
1995 renderables = hook.process_renderables(renderables)
1996 new_segments: List[Segment] = []
1997 extend = new_segments.extend
1998 render = self.render
1999 render_options = self.options
2000 for renderable in renderables:
2001 extend(render(renderable, render_options))
2002 buffer_extend = self._buffer.extend
2003 for line in Segment.split_and_crop_lines(
2004 new_segments, self.width, pad=False
2005 ):
2006 buffer_extend(line)
2008 def on_broken_pipe(self) -> None:
2009 """This function is called when a `BrokenPipeError` is raised.
2011 This can occur when piping Textual output in Linux and macOS.
2012 The default implementation is to exit the app, but you could implement
2013 this method in a subclass to change the behavior.
2015 See https://docs.python.org/3/library/signal.html#note-on-sigpipe for details.
2016 """
2017 self.quiet = True
2018 devnull = os.open(os.devnull, os.O_WRONLY)
2019 os.dup2(devnull, sys.stdout.fileno())
2020 raise SystemExit(1)
2022 def _check_buffer(self) -> None:
2023 """Check if the buffer may be rendered. Render it if it can (e.g. Console.quiet is False)
2024 Rendering is supported on Windows, Unix and Jupyter environments. For
2025 legacy Windows consoles, the win32 API is called directly.
2026 This method will also record what it renders if recording is enabled via Console.record.
2027 """
2028 if self.quiet:
2029 del self._buffer[:]
2030 return
2032 try:
2033 self._write_buffer()
2034 except BrokenPipeError:
2035 self.on_broken_pipe()
2037 def _write_buffer(self) -> None:
2038 """Write the buffer to the output file."""
2040 with self._lock:
2041 if self.record and not self._buffer_index:
2042 with self._record_buffer_lock:
2043 self._record_buffer.extend(self._buffer[:])
2045 if self._buffer_index == 0:
2046 if self.is_jupyter: # pragma: no cover
2047 from .jupyter import display
2049 display(self._buffer, self._render_buffer(self._buffer[:]))
2050 del self._buffer[:]
2051 else:
2052 if WINDOWS:
2053 use_legacy_windows_render = False
2054 if self.legacy_windows:
2055 fileno = get_fileno(self.file)
2056 if fileno is not None:
2057 use_legacy_windows_render = (
2058 fileno in _STD_STREAMS_OUTPUT
2059 )
2061 if use_legacy_windows_render:
2062 from rich._win32_console import LegacyWindowsTerm
2063 from rich._windows_renderer import legacy_windows_render
2065 buffer = self._buffer[:]
2066 if self.no_color and self._color_system:
2067 buffer = list(Segment.remove_color(buffer))
2069 legacy_windows_render(buffer, LegacyWindowsTerm(self.file))
2070 else:
2071 # Either a non-std stream on legacy Windows, or modern Windows.
2072 text = self._render_buffer(self._buffer[:])
2073 # https://bugs.python.org/issue37871
2074 # https://github.com/python/cpython/issues/82052
2075 # We need to avoid writing more than 32Kb in a single write, due to the above bug
2076 write = self.file.write
2077 # Worse case scenario, every character is 4 bytes of utf-8
2078 MAX_WRITE = 32 * 1024 // 4
2079 try:
2080 if len(text) <= MAX_WRITE:
2081 write(text)
2082 else:
2083 batch: List[str] = []
2084 batch_append = batch.append
2085 size = 0
2086 for line in text.splitlines(True):
2087 if size + len(line) > MAX_WRITE and batch:
2088 write("".join(batch))
2089 batch.clear()
2090 size = 0
2091 batch_append(line)
2092 size += len(line)
2093 if batch:
2094 write("".join(batch))
2095 batch.clear()
2096 except UnicodeEncodeError as error:
2097 error.reason = f"{error.reason}\n*** You may need to add PYTHONIOENCODING=utf-8 to your environment ***"
2098 raise
2099 else:
2100 text = self._render_buffer(self._buffer[:])
2101 try:
2102 self.file.write(text)
2103 except UnicodeEncodeError as error:
2104 error.reason = f"{error.reason}\n*** You may need to add PYTHONIOENCODING=utf-8 to your environment ***"
2105 raise
2107 self.file.flush()
2108 del self._buffer[:]
2110 def _render_buffer(self, buffer: Iterable[Segment]) -> str:
2111 """Render buffered output, and clear buffer."""
2112 output: List[str] = []
2113 append = output.append
2114 color_system = self._color_system
2115 legacy_windows = self.legacy_windows
2116 not_terminal = not self.is_terminal
2117 if self.no_color and color_system:
2118 buffer = Segment.remove_color(buffer)
2119 for text, style, control in buffer:
2120 if style:
2121 append(
2122 style.render(
2123 text,
2124 color_system=color_system,
2125 legacy_windows=legacy_windows,
2126 )
2127 )
2128 elif not (not_terminal and control):
2129 append(text)
2131 rendered = "".join(output)
2132 return rendered
2134 def input(
2135 self,
2136 prompt: TextType = "",
2137 *,
2138 markup: bool = True,
2139 emoji: bool = True,
2140 password: bool = False,
2141 stream: Optional[TextIO] = None,
2142 ) -> str:
2143 """Displays a prompt and waits for input from the user. The prompt may contain color / style.
2145 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.
2147 Args:
2148 prompt (Union[str, Text]): Text to render in the prompt.
2149 markup (bool, optional): Enable console markup (requires a str prompt). Defaults to True.
2150 emoji (bool, optional): Enable emoji (requires a str prompt). Defaults to True.
2151 password: (bool, optional): Hide typed text. Defaults to False.
2152 stream: (TextIO, optional): Optional file to read input from (rather than stdin). Defaults to None.
2154 Returns:
2155 str: Text read from stdin.
2156 """
2157 if prompt:
2158 self.print(prompt, markup=markup, emoji=emoji, end="")
2159 if password:
2160 result = getpass("", stream=stream)
2161 else:
2162 if stream:
2163 result = stream.readline()
2164 else:
2165 result = input()
2166 return result
2168 def export_text(self, *, clear: bool = True, styles: bool = False) -> str:
2169 """Generate text from console contents (requires record=True argument in constructor).
2171 Args:
2172 clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``.
2173 styles (bool, optional): If ``True``, ansi escape codes will be included. ``False`` for plain text.
2174 Defaults to ``False``.
2176 Returns:
2177 str: String containing console contents.
2179 """
2180 assert (
2181 self.record
2182 ), "To export console contents set record=True in the constructor or instance"
2184 with self._record_buffer_lock:
2185 if styles:
2186 text = "".join(
2187 (style.render(text) if style else text)
2188 for text, style, _ in self._record_buffer
2189 )
2190 else:
2191 text = "".join(
2192 segment.text
2193 for segment in self._record_buffer
2194 if not segment.control
2195 )
2196 if clear:
2197 del self._record_buffer[:]
2198 return text
2200 def save_text(self, path: str, *, clear: bool = True, styles: bool = False) -> None:
2201 """Generate text from console and save to a given location (requires record=True argument in constructor).
2203 Args:
2204 path (str): Path to write text files.
2205 clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``.
2206 styles (bool, optional): If ``True``, ansi style codes will be included. ``False`` for plain text.
2207 Defaults to ``False``.
2209 """
2210 text = self.export_text(clear=clear, styles=styles)
2211 with open(path, "w", encoding="utf-8") as write_file:
2212 write_file.write(text)
2214 def export_html(
2215 self,
2216 *,
2217 theme: Optional[TerminalTheme] = None,
2218 clear: bool = True,
2219 code_format: Optional[str] = None,
2220 inline_styles: bool = False,
2221 ) -> str:
2222 """Generate HTML from console contents (requires record=True argument in constructor).
2224 Args:
2225 theme (TerminalTheme, optional): TerminalTheme object containing console colors.
2226 clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``.
2227 code_format (str, optional): Format string to render HTML. In addition to '{foreground}',
2228 '{background}', and '{code}', should contain '{stylesheet}' if inline_styles is ``False``.
2229 inline_styles (bool, optional): If ``True`` styles will be inlined in to spans, which makes files
2230 larger but easier to cut and paste markup. If ``False``, styles will be embedded in a style tag.
2231 Defaults to False.
2233 Returns:
2234 str: String containing console contents as HTML.
2235 """
2236 assert (
2237 self.record
2238 ), "To export console contents set record=True in the constructor or instance"
2239 fragments: List[str] = []
2240 append = fragments.append
2241 _theme = theme or DEFAULT_TERMINAL_THEME
2242 stylesheet = ""
2244 render_code_format = CONSOLE_HTML_FORMAT if code_format is None else code_format
2246 with self._record_buffer_lock:
2247 if inline_styles:
2248 for text, style, _ in Segment.filter_control(
2249 Segment.simplify(self._record_buffer)
2250 ):
2251 text = escape(text)
2252 if style:
2253 rule = style.get_html_style(_theme)
2254 if style.link:
2255 text = f'<a href="{style.link}">{text}</a>'
2256 text = f'<span style="{rule}">{text}</span>' if rule else text
2257 append(text)
2258 else:
2259 styles: Dict[str, int] = {}
2260 for text, style, _ in Segment.filter_control(
2261 Segment.simplify(self._record_buffer)
2262 ):
2263 text = escape(text)
2264 if style:
2265 rule = style.get_html_style(_theme)
2266 style_number = styles.setdefault(rule, len(styles) + 1)
2267 if style.link:
2268 text = f'<a class="r{style_number}" href="{style.link}">{text}</a>'
2269 else:
2270 text = f'<span class="r{style_number}">{text}</span>'
2271 append(text)
2272 stylesheet_rules: List[str] = []
2273 stylesheet_append = stylesheet_rules.append
2274 for style_rule, style_number in styles.items():
2275 if style_rule:
2276 stylesheet_append(f".r{style_number} {{{style_rule}}}")
2277 stylesheet = "\n".join(stylesheet_rules)
2279 rendered_code = render_code_format.format(
2280 code="".join(fragments),
2281 stylesheet=stylesheet,
2282 foreground=_theme.foreground_color.hex,
2283 background=_theme.background_color.hex,
2284 )
2285 if clear:
2286 del self._record_buffer[:]
2287 return rendered_code
2289 def save_html(
2290 self,
2291 path: str,
2292 *,
2293 theme: Optional[TerminalTheme] = None,
2294 clear: bool = True,
2295 code_format: str = CONSOLE_HTML_FORMAT,
2296 inline_styles: bool = False,
2297 ) -> None:
2298 """Generate HTML from console contents and write to a file (requires record=True argument in constructor).
2300 Args:
2301 path (str): Path to write html file.
2302 theme (TerminalTheme, optional): TerminalTheme object containing console colors.
2303 clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``.
2304 code_format (str, optional): Format string to render HTML. In addition to '{foreground}',
2305 '{background}', and '{code}', should contain '{stylesheet}' if inline_styles is ``False``.
2306 inline_styles (bool, optional): If ``True`` styles will be inlined in to spans, which makes files
2307 larger but easier to cut and paste markup. If ``False``, styles will be embedded in a style tag.
2308 Defaults to False.
2310 """
2311 html = self.export_html(
2312 theme=theme,
2313 clear=clear,
2314 code_format=code_format,
2315 inline_styles=inline_styles,
2316 )
2317 with open(path, "w", encoding="utf-8") as write_file:
2318 write_file.write(html)
2320 def export_svg(
2321 self,
2322 *,
2323 title: str = "Rich",
2324 theme: Optional[TerminalTheme] = None,
2325 clear: bool = True,
2326 code_format: str = CONSOLE_SVG_FORMAT,
2327 font_aspect_ratio: float = 0.61,
2328 unique_id: Optional[str] = None,
2329 ) -> str:
2330 """
2331 Generate an SVG from the console contents (requires record=True in Console constructor).
2333 Args:
2334 title (str, optional): The title of the tab in the output image
2335 theme (TerminalTheme, optional): The ``TerminalTheme`` object to use to style the terminal
2336 clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``
2337 code_format (str, optional): Format string used to generate the SVG. Rich will inject a number of variables
2338 into the string in order to form the final SVG output. The default template used and the variables
2339 injected by Rich can be found by inspecting the ``console.CONSOLE_SVG_FORMAT`` variable.
2340 font_aspect_ratio (float, optional): The width to height ratio of the font used in the ``code_format``
2341 string. Defaults to 0.61, which is the width to height ratio of Fira Code (the default font).
2342 If you aren't specifying a different font inside ``code_format``, you probably don't need this.
2343 unique_id (str, optional): unique id that is used as the prefix for various elements (CSS styles, node
2344 ids). If not set, this defaults to a computed value based on the recorded content.
2345 """
2347 from rich.cells import cell_len
2349 style_cache: Dict[Style, str] = {}
2351 def get_svg_style(style: Style) -> str:
2352 """Convert a Style to CSS rules for SVG."""
2353 if style in style_cache:
2354 return style_cache[style]
2355 css_rules = []
2356 color = (
2357 _theme.foreground_color
2358 if (style.color is None or style.color.is_default)
2359 else style.color.get_truecolor(_theme)
2360 )
2361 bgcolor = (
2362 _theme.background_color
2363 if (style.bgcolor is None or style.bgcolor.is_default)
2364 else style.bgcolor.get_truecolor(_theme)
2365 )
2366 if style.reverse:
2367 color, bgcolor = bgcolor, color
2368 if style.dim:
2369 color = blend_rgb(color, bgcolor, 0.4)
2370 css_rules.append(f"fill: {color.hex}")
2371 if style.bold:
2372 css_rules.append("font-weight: bold")
2373 if style.italic:
2374 css_rules.append("font-style: italic;")
2375 if style.underline:
2376 css_rules.append("text-decoration: underline;")
2377 if style.strike:
2378 css_rules.append("text-decoration: line-through;")
2380 css = ";".join(css_rules)
2381 style_cache[style] = css
2382 return css
2384 _theme = theme or SVG_EXPORT_THEME
2386 width = self.width
2387 char_height = 20
2388 char_width = char_height * font_aspect_ratio
2389 line_height = char_height * 1.22
2391 margin_top = 1
2392 margin_right = 1
2393 margin_bottom = 1
2394 margin_left = 1
2396 padding_top = 40
2397 padding_right = 8
2398 padding_bottom = 8
2399 padding_left = 8
2401 padding_width = padding_left + padding_right
2402 padding_height = padding_top + padding_bottom
2403 margin_width = margin_left + margin_right
2404 margin_height = margin_top + margin_bottom
2406 text_backgrounds: List[str] = []
2407 text_group: List[str] = []
2408 classes: Dict[str, int] = {}
2409 style_no = 1
2411 def escape_text(text: str) -> str:
2412 """HTML escape text and replace spaces with nbsp."""
2413 return escape(text).replace(" ", " ")
2415 def make_tag(
2416 name: str, content: Optional[str] = None, **attribs: object
2417 ) -> str:
2418 """Make a tag from name, content, and attributes."""
2420 def stringify(value: object) -> str:
2421 if isinstance(value, (float)):
2422 return format(value, "g")
2423 return str(value)
2425 tag_attribs = " ".join(
2426 f'{k.lstrip("_").replace("_", "-")}="{stringify(v)}"'
2427 for k, v in attribs.items()
2428 )
2429 return (
2430 f"<{name} {tag_attribs}>{content}</{name}>"
2431 if content
2432 else f"<{name} {tag_attribs}/>"
2433 )
2435 with self._record_buffer_lock:
2436 segments = list(Segment.filter_control(self._record_buffer))
2437 if clear:
2438 self._record_buffer.clear()
2440 if unique_id is None:
2441 unique_id = "terminal-" + str(
2442 zlib.adler32(
2443 ("".join(repr(segment) for segment in segments)).encode(
2444 "utf-8",
2445 "ignore",
2446 )
2447 + title.encode("utf-8", "ignore")
2448 )
2449 )
2450 y = 0
2451 for y, line in enumerate(Segment.split_and_crop_lines(segments, length=width)):
2452 x = 0
2453 for text, style, _control in line:
2454 style = style or Style()
2455 rules = get_svg_style(style)
2456 if rules not in classes:
2457 classes[rules] = style_no
2458 style_no += 1
2459 class_name = f"r{classes[rules]}"
2461 if style.reverse:
2462 has_background = True
2463 background = (
2464 _theme.foreground_color.hex
2465 if style.color is None
2466 else style.color.get_truecolor(_theme).hex
2467 )
2468 else:
2469 bgcolor = style.bgcolor
2470 has_background = bgcolor is not None and not bgcolor.is_default
2471 background = (
2472 _theme.background_color.hex
2473 if style.bgcolor is None
2474 else style.bgcolor.get_truecolor(_theme).hex
2475 )
2477 text_length = cell_len(text)
2478 if has_background:
2479 text_backgrounds.append(
2480 make_tag(
2481 "rect",
2482 fill=background,
2483 x=x * char_width,
2484 y=y * line_height + 1.5,
2485 width=char_width * text_length,
2486 height=line_height + 0.25,
2487 shape_rendering="crispEdges",
2488 )
2489 )
2491 if text != " " * len(text):
2492 text_group.append(
2493 make_tag(
2494 "text",
2495 escape_text(text),
2496 _class=f"{unique_id}-{class_name}",
2497 x=x * char_width,
2498 y=y * line_height + char_height,
2499 textLength=char_width * len(text),
2500 clip_path=f"url(#{unique_id}-line-{y})",
2501 )
2502 )
2503 x += cell_len(text)
2505 line_offsets = [line_no * line_height + 1.5 for line_no in range(y)]
2506 lines = "\n".join(
2507 f"""<clipPath id="{unique_id}-line-{line_no}">
2508 {make_tag("rect", x=0, y=offset, width=char_width * width, height=line_height + 0.25)}
2509 </clipPath>"""
2510 for line_no, offset in enumerate(line_offsets)
2511 )
2513 styles = "\n".join(
2514 f".{unique_id}-r{rule_no} {{ {css} }}" for css, rule_no in classes.items()
2515 )
2516 backgrounds = "".join(text_backgrounds)
2517 matrix = "".join(text_group)
2519 terminal_width = ceil(width * char_width + padding_width)
2520 terminal_height = (y + 1) * line_height + padding_height
2521 chrome = make_tag(
2522 "rect",
2523 fill=_theme.background_color.hex,
2524 stroke="rgba(255,255,255,0.35)",
2525 stroke_width="1",
2526 x=margin_left,
2527 y=margin_top,
2528 width=terminal_width,
2529 height=terminal_height,
2530 rx=8,
2531 )
2533 title_color = _theme.foreground_color.hex
2534 if title:
2535 chrome += make_tag(
2536 "text",
2537 escape_text(title),
2538 _class=f"{unique_id}-title",
2539 fill=title_color,
2540 text_anchor="middle",
2541 x=terminal_width // 2,
2542 y=margin_top + char_height + 6,
2543 )
2544 chrome += f"""
2545 <g transform="translate(26,22)">
2546 <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
2547 <circle cx="22" cy="0" r="7" fill="#febc2e"/>
2548 <circle cx="44" cy="0" r="7" fill="#28c840"/>
2549 </g>
2550 """
2552 svg = code_format.format(
2553 unique_id=unique_id,
2554 char_width=char_width,
2555 char_height=char_height,
2556 line_height=line_height,
2557 terminal_width=char_width * width - 1,
2558 terminal_height=(y + 1) * line_height - 1,
2559 width=terminal_width + margin_width,
2560 height=terminal_height + margin_height,
2561 terminal_x=margin_left + padding_left,
2562 terminal_y=margin_top + padding_top,
2563 styles=styles,
2564 chrome=chrome,
2565 backgrounds=backgrounds,
2566 matrix=matrix,
2567 lines=lines,
2568 )
2569 return svg
2571 def save_svg(
2572 self,
2573 path: str,
2574 *,
2575 title: str = "Rich",
2576 theme: Optional[TerminalTheme] = None,
2577 clear: bool = True,
2578 code_format: str = CONSOLE_SVG_FORMAT,
2579 font_aspect_ratio: float = 0.61,
2580 unique_id: Optional[str] = None,
2581 ) -> None:
2582 """Generate an SVG file from the console contents (requires record=True in Console constructor).
2584 Args:
2585 path (str): The path to write the SVG to.
2586 title (str, optional): The title of the tab in the output image
2587 theme (TerminalTheme, optional): The ``TerminalTheme`` object to use to style the terminal
2588 clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``
2589 code_format (str, optional): Format string used to generate the SVG. Rich will inject a number of variables
2590 into the string in order to form the final SVG output. The default template used and the variables
2591 injected by Rich can be found by inspecting the ``console.CONSOLE_SVG_FORMAT`` variable.
2592 font_aspect_ratio (float, optional): The width to height ratio of the font used in the ``code_format``
2593 string. Defaults to 0.61, which is the width to height ratio of Fira Code (the default font).
2594 If you aren't specifying a different font inside ``code_format``, you probably don't need this.
2595 unique_id (str, optional): unique id that is used as the prefix for various elements (CSS styles, node
2596 ids). If not set, this defaults to a computed value based on the recorded content.
2597 """
2598 svg = self.export_svg(
2599 title=title,
2600 theme=theme,
2601 clear=clear,
2602 code_format=code_format,
2603 font_aspect_ratio=font_aspect_ratio,
2604 unique_id=unique_id,
2605 )
2606 with open(path, "w", encoding="utf-8") as write_file:
2607 write_file.write(svg)
2610def _svg_hash(svg_main_code: str) -> str:
2611 """Returns a unique hash for the given SVG main code.
2613 Args:
2614 svg_main_code (str): The content we're going to inject in the SVG envelope.
2616 Returns:
2617 str: a hash of the given content
2618 """
2619 return str(zlib.adler32(svg_main_code.encode()))
2622if __name__ == "__main__": # pragma: no cover
2623 console = Console(record=True)
2625 console.log(
2626 "JSONRPC [i]request[/i]",
2627 5,
2628 1.3,
2629 True,
2630 False,
2631 None,
2632 {
2633 "jsonrpc": "2.0",
2634 "method": "subtract",
2635 "params": {"minuend": 42, "subtrahend": 23},
2636 "id": 3,
2637 },
2638 )
2640 console.log("Hello, World!", "{'a': 1}", repr(console))
2642 console.print(
2643 {
2644 "name": None,
2645 "empty": [],
2646 "quiz": {
2647 "sport": {
2648 "answered": True,
2649 "q1": {
2650 "question": "Which one is correct team name in NBA?",
2651 "options": [
2652 "New York Bulls",
2653 "Los Angeles Kings",
2654 "Golden State Warriors",
2655 "Huston Rocket",
2656 ],
2657 "answer": "Huston Rocket",
2658 },
2659 },
2660 "maths": {
2661 "answered": False,
2662 "q1": {
2663 "question": "5 + 7 = ?",
2664 "options": [10, 11, 12, 13],
2665 "answer": 12,
2666 },
2667 "q2": {
2668 "question": "12 - 8 = ?",
2669 "options": [1, 2, 3, 4],
2670 "answer": 4,
2671 },
2672 },
2673 },
2674 }
2675 )