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