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