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)
37
38from pip._vendor.rich._null_file import NULL_FILE
39
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
64
65if TYPE_CHECKING:
66 from ._windows import WindowsConsoleFeatures
67 from .live import Live
68 from .status import Status
69
70JUPYTER_DEFAULT_COLUMNS = 115
71JUPYTER_DEFAULT_LINES = 100
72WINDOWS = sys.platform == "win32"
73
74HighlighterType = Callable[[Union[str, "Text"]], "Text"]
75JustifyMethod = Literal["default", "left", "center", "right", "full"]
76OverflowMethod = Literal["fold", "crop", "ellipsis", "ignore"]
77
78
79class NoChange:
80 pass
81
82
83NO_CHANGE = NoChange()
84
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
97
98_STD_STREAMS = (_STDIN_FILENO, _STDOUT_FILENO, _STDERR_FILENO)
99_STD_STREAMS_OUTPUT = (_STDOUT_FILENO, _STDERR_FILENO)
100
101
102_TERM_COLORS = {
103 "kitty": ColorSystem.EIGHT_BIT,
104 "256color": ColorSystem.EIGHT_BIT,
105 "16color": ColorSystem.STANDARD,
106}
107
108
109class ConsoleDimensions(NamedTuple):
110 """Size of the terminal."""
111
112 width: int
113 """The width of the console in 'cells'."""
114 height: int
115 """The height of the console in lines."""
116
117
118@dataclass
119class ConsoleOptions:
120 """Options for __rich_console__ method."""
121
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
147
148 @property
149 def ascii_only(self) -> bool:
150 """Check if renderables should use ascii only."""
151 return not self.encoding.startswith("utf")
152
153 def copy(self) -> "ConsoleOptions":
154 """Return a copy of the options.
155
156 Returns:
157 ConsoleOptions: a copy of self.
158 """
159 options: ConsoleOptions = ConsoleOptions.__new__(ConsoleOptions)
160 options.__dict__ = self.__dict__.copy()
161 return options
162
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
199
200 def update_width(self, width: int) -> "ConsoleOptions":
201 """Update just the width, return a copy.
202
203 Args:
204 width (int): New width (sets both min_width and max_width)
205
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
212
213 def update_height(self, height: int) -> "ConsoleOptions":
214 """Update the height, and return a copy.
215
216 Args:
217 height (int): New height
218
219 Returns:
220 ~ConsoleOptions: New Console options instance.
221 """
222 options = self.copy()
223 options.max_height = options.height = height
224 return options
225
226 def reset_height(self) -> "ConsoleOptions":
227 """Return a copy of the options with height set to ``None``.
228
229 Returns:
230 ~ConsoleOptions: New console options instance.
231 """
232 options = self.copy()
233 options.height = None
234 return options
235
236 def update_dimensions(self, width: int, height: int) -> "ConsoleOptions":
237 """Update the width and height, and return a copy.
238
239 Args:
240 width (int): New width (sets both min_width and max_width).
241 height (int): New height.
242
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
250
251
252@runtime_checkable
253class RichCast(Protocol):
254 """An object that may be 'cast' to a console renderable."""
255
256 def __rich__(
257 self,
258 ) -> Union["ConsoleRenderable", "RichCast", str]: # pragma: no cover
259 ...
260
261
262@runtime_checkable
263class ConsoleRenderable(Protocol):
264 """An object that supports the console protocol."""
265
266 def __rich_console__(
267 self, console: "Console", options: "ConsoleOptions"
268 ) -> "RenderResult": # pragma: no cover
269 ...
270
271
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."""
275
276# The result of calling a __rich_console__ method.
277RenderResult = Iterable[Union[RenderableType, Segment]]
278
279_null_highlighter = NullHighlighter()
280
281
282class CaptureError(Exception):
283 """An error in the Capture context manager."""
284
285
286class NewLine:
287 """A renderable to generate new line(s)"""
288
289 def __init__(self, count: int = 1) -> None:
290 self.count = count
291
292 def __rich_console__(
293 self, console: "Console", options: "ConsoleOptions"
294 ) -> Iterable[Segment]:
295 yield Segment("\n" * self.count)
296
297
298class ScreenUpdate:
299 """Render a list of lines at a given offset."""
300
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
305
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
314
315
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.
319
320 Args:
321 console (Console): A console instance to capture output.
322 """
323
324 def __init__(self, console: "Console") -> None:
325 self._console = console
326 self._result: Optional[str] = None
327
328 def __enter__(self) -> "Capture":
329 self._console.begin_capture()
330 return self
331
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()
339
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
347
348
349class ThemeContext:
350 """A context manager to use a temporary theme. See :meth:`~rich.console.Console.use_theme` for usage."""
351
352 def __init__(self, console: "Console", theme: Theme, inherit: bool = True) -> None:
353 self.console = console
354 self.theme = theme
355 self.inherit = inherit
356
357 def __enter__(self) -> "ThemeContext":
358 self.console.push_theme(self.theme)
359 return self
360
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()
368
369
370class PagerContext:
371 """A context manager that 'pages' content. See :meth:`~rich.console.Console.pager` for usage."""
372
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
384
385 def __enter__(self) -> "PagerContext":
386 self._console._enter_buffer()
387 return self
388
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()
407
408
409class ScreenContext:
410 """A context manager that enables an alternative screen. See :meth:`~rich.console.Console.screen` for usage."""
411
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
419
420 def update(
421 self, *renderables: RenderableType, style: Optional[StyleType] = None
422 ) -> None:
423 """Update the screen.
424
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="")
437
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
443
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)
454
455
456class Group:
457 """Takes a group of renderables and returns a renderable object that renders the group.
458
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 """
463
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
468
469 @property
470 def renderables(self) -> List["RenderableType"]:
471 if self._render is None:
472 self._render = list(self._renderables)
473 return self._render
474
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)
482
483 def __rich_console__(
484 self, console: "Console", options: "ConsoleOptions"
485 ) -> RenderResult:
486 yield from self.renderables
487
488
489def group(fit: bool = True) -> Callable[..., Callable[..., Group]]:
490 """A decorator that turns an iterable of renderables in to a group.
491
492 Args:
493 fit (bool, optional): Fit dimension of group to contents, or fill available space. Defaults to True.
494 """
495
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."""
500
501 @wraps(method)
502 def _replace(*args: Any, **kwargs: Any) -> Group:
503 renderables = method(*args, **kwargs)
504 return Group(*renderables, fit=fit)
505
506 return _replace
507
508 return decorator
509
510
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 (?)
529
530
531COLOR_SYSTEMS = {
532 "standard": ColorSystem.STANDARD,
533 "256": ColorSystem.EIGHT_BIT,
534 "truecolor": ColorSystem.TRUECOLOR,
535 "windows": ColorSystem.WINDOWS,
536}
537
538_COLOR_SYSTEMS_NAMES = {system: name for name, system in COLOR_SYSTEMS.items()}
539
540
541@dataclass
542class ConsoleThreadLocals(threading.local):
543 """Thread local values for Console context."""
544
545 theme_stack: ThemeStack
546 buffer: List[Segment] = field(default_factory=list)
547 buffer_index: int = 0
548
549
550class RenderHook(ABC):
551 """Provides hooks in to the render process."""
552
553 @abstractmethod
554 def process_renderables(
555 self, renderables: List[ConsoleRenderable]
556 ) -> List[ConsoleRenderable]:
557 """Called with a list of objects to render.
558
559 This method can return a new list of renderables, or modify and return the same list.
560
561 Args:
562 renderables (List[ConsoleRenderable]): A number of renderable objects.
563
564 Returns:
565 List[ConsoleRenderable]: A replacement list of renderables.
566 """
567
568
569_windows_console_features: Optional["WindowsConsoleFeatures"] = None
570
571
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
577
578 _windows_console_features = get_windows_console_features()
579 return _windows_console_features
580
581
582def detect_legacy_windows() -> bool:
583 """Detect legacy Windows."""
584 return WINDOWS and not get_windows_console_features().vt
585
586
587class Console:
588 """A high level console interface.
589
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 """
622
623 _environ: Mapping[str, str] = os.environ
624
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
662
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
677
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 )
689
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)
698
699 self.soft_wrap = soft_wrap
700 self._width = width
701 self._height = height
702
703 self._color_system: Optional[ColorSystem]
704
705 self._force_terminal = None
706 if force_terminal is not None:
707 self._force_terminal = force_terminal
708
709 self._file = file
710 self.quiet = quiet
711 self.stderr = stderr
712
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]
719
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
743
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 )
749
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
758
759 def __repr__(self) -> str:
760 return f"<console width={self.width} {self._color_system!s}>"
761
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
770
771 @file.setter
772 def file(self, new_file: IO[str]) -> None:
773 """Set a new file object."""
774 self._file = new_file
775
776 @property
777 def _buffer(self) -> List[Segment]:
778 """Get a thread local buffer."""
779 return self._thread_locals.buffer
780
781 @property
782 def _buffer_index(self) -> int:
783 """Get a thread local buffer."""
784 return self._thread_locals.buffer_index
785
786 @_buffer_index.setter
787 def _buffer_index(self, value: int) -> None:
788 self._thread_locals.buffer_index = value
789
790 @property
791 def _theme_stack(self) -> ThemeStack:
792 """Get the thread local theme stack."""
793 return self._thread_locals.theme_stack
794
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
818
819 def _enter_buffer(self) -> None:
820 """Enter in to a buffer context, and buffer all output."""
821 self._buffer_index += 1
822
823 def _exit_buffer(self) -> None:
824 """Leave buffer context, and render content if required."""
825 self._buffer_index -= 1
826 self._check_buffer()
827
828 def set_live(self, live: "Live") -> bool:
829 """Set Live instance. Used by Live context manager (no need to call directly).
830
831 Args:
832 live (Live): Live instance using this Console.
833
834 Returns:
835 Boolean that indicates if the live is the topmost of the stack.
836
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
843
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()
848
849 def push_render_hook(self, hook: RenderHook) -> None:
850 """Add a new render hook to the stack.
851
852 Args:
853 hook (RenderHook): Render hook instance.
854 """
855 with self._lock:
856 self._render_hooks.append(hook)
857
858 def pop_render_hook(self) -> None:
859 """Pop the last renderhook from the stack."""
860 with self._lock:
861 self._render_hooks.pop()
862
863 def __enter__(self) -> "Console":
864 """Own context manager to enter buffer context."""
865 self._enter_buffer()
866 return self
867
868 def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
869 """Exit buffer context."""
870 self._exit_buffer()
871
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()
875
876 def end_capture(self) -> str:
877 """End capture mode and return captured string.
878
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
886
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.
891
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)
897
898 def pop_theme(self) -> None:
899 """Remove theme from top of stack, restoring previous theme."""
900 self._theme_stack.pop_theme()
901
902 def use_theme(self, theme: Theme, *, inherit: bool = True) -> ThemeContext:
903 """Use a different theme for the duration of the context manager.
904
905 Args:
906 theme (Theme): Theme instance to user.
907 inherit (bool, optional): Inherit existing console styles. Defaults to True.
908
909 Returns:
910 ThemeContext: [description]
911 """
912 return ThemeContext(self, theme, inherit)
913
914 @property
915 def color_system(self) -> Optional[str]:
916 """Get color system string.
917
918 Returns:
919 Optional[str]: "standard", "256" or "truecolor".
920 """
921
922 if self._color_system is not None:
923 return _COLOR_SYSTEMS_NAMES[self._color_system]
924 else:
925 return None
926
927 @property
928 def encoding(self) -> str:
929 """Get the encoding of the console file, e.g. ``"utf-8"``.
930
931 Returns:
932 str: A standard encoding string.
933 """
934 return (getattr(self.file, "encoding", "utf-8") or "utf-8").lower()
935
936 @property
937 def is_terminal(self) -> bool:
938 """Check if the console is writing to a terminal.
939
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
947
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
954
955 if self.is_jupyter:
956 # return False for Jupyter, which may have FORCE_COLOR set
957 return False
958
959 environ = self._environ
960
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
968
969 # https://force-color.org/
970 force_color = environ.get("FORCE_COLOR")
971 if force_color is not None:
972 return force_color != ""
973
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
983
984 @property
985 def is_dumb_terminal(self) -> bool:
986 """Detect dumb terminal.
987
988 Returns:
989 bool: True if writing to a dumb terminal, otherwise False.
990
991 """
992 _term = self._environ.get("TERM", "")
993 is_dumb = _term.lower() in ("dumb", "unknown")
994 return self.is_terminal and is_dumb
995
996 @property
997 def options(self) -> ConsoleOptions:
998 """Get default console options."""
999 size = self.size
1000 return ConsoleOptions(
1001 max_height=size.height,
1002 size=size,
1003 legacy_windows=self.legacy_windows,
1004 min_width=1,
1005 max_width=size.width,
1006 encoding=self.encoding,
1007 is_terminal=self.is_terminal,
1008 )
1009
1010 @property
1011 def size(self) -> ConsoleDimensions:
1012 """Get the size of the console.
1013
1014 Returns:
1015 ConsoleDimensions: A named tuple containing the dimensions.
1016 """
1017
1018 if self._width is not None and self._height is not None:
1019 return ConsoleDimensions(self._width - self.legacy_windows, self._height)
1020
1021 if self.is_dumb_terminal:
1022 return ConsoleDimensions(80, 25)
1023
1024 width: Optional[int] = None
1025 height: Optional[int] = None
1026
1027 streams = _STD_STREAMS_OUTPUT if WINDOWS else _STD_STREAMS
1028 for file_descriptor in streams:
1029 try:
1030 width, height = os.get_terminal_size(file_descriptor)
1031 except (AttributeError, ValueError, OSError): # Probably not a terminal
1032 pass
1033 else:
1034 break
1035
1036 columns = self._environ.get("COLUMNS")
1037 if columns is not None and columns.isdigit():
1038 width = int(columns)
1039 lines = self._environ.get("LINES")
1040 if lines is not None and lines.isdigit():
1041 height = int(lines)
1042
1043 # get_terminal_size can report 0, 0 if run from pseudo-terminal
1044 width = width or 80
1045 height = height or 25
1046 return ConsoleDimensions(
1047 width - self.legacy_windows if self._width is None else self._width,
1048 height if self._height is None else self._height,
1049 )
1050
1051 @size.setter
1052 def size(self, new_size: Tuple[int, int]) -> None:
1053 """Set a new size for the terminal.
1054
1055 Args:
1056 new_size (Tuple[int, int]): New width and height.
1057 """
1058 width, height = new_size
1059 self._width = width
1060 self._height = height
1061
1062 @property
1063 def width(self) -> int:
1064 """Get the width of the console.
1065
1066 Returns:
1067 int: The width (in characters) of the console.
1068 """
1069 return self.size.width
1070
1071 @width.setter
1072 def width(self, width: int) -> None:
1073 """Set width.
1074
1075 Args:
1076 width (int): New width.
1077 """
1078 self._width = width
1079
1080 @property
1081 def height(self) -> int:
1082 """Get the height of the console.
1083
1084 Returns:
1085 int: The height (in lines) of the console.
1086 """
1087 return self.size.height
1088
1089 @height.setter
1090 def height(self, height: int) -> None:
1091 """Set height.
1092
1093 Args:
1094 height (int): new height.
1095 """
1096 self._height = height
1097
1098 def bell(self) -> None:
1099 """Play a 'bell' sound (if supported by the terminal)."""
1100 self.control(Control.bell())
1101
1102 def capture(self) -> Capture:
1103 """A context manager to *capture* the result of print() or log() in a string,
1104 rather than writing it to the console.
1105
1106 Example:
1107 >>> from rich.console import Console
1108 >>> console = Console()
1109 >>> with console.capture() as capture:
1110 ... console.print("[bold magenta]Hello World[/]")
1111 >>> print(capture.get())
1112
1113 Returns:
1114 Capture: Context manager with disables writing to the terminal.
1115 """
1116 capture = Capture(self)
1117 return capture
1118
1119 def pager(
1120 self, pager: Optional[Pager] = None, styles: bool = False, links: bool = False
1121 ) -> PagerContext:
1122 """A context manager to display anything printed within a "pager". The pager application
1123 is defined by the system and will typically support at least pressing a key to scroll.
1124
1125 Args:
1126 pager (Pager, optional): A pager object, or None to use :class:`~rich.pager.SystemPager`. Defaults to None.
1127 styles (bool, optional): Show styles in pager. Defaults to False.
1128 links (bool, optional): Show links in pager. Defaults to False.
1129
1130 Example:
1131 >>> from rich.console import Console
1132 >>> from rich.__main__ import make_test_card
1133 >>> console = Console()
1134 >>> with console.pager():
1135 console.print(make_test_card())
1136
1137 Returns:
1138 PagerContext: A context manager.
1139 """
1140 return PagerContext(self, pager=pager, styles=styles, links=links)
1141
1142 def line(self, count: int = 1) -> None:
1143 """Write new line(s).
1144
1145 Args:
1146 count (int, optional): Number of new lines. Defaults to 1.
1147 """
1148
1149 assert count >= 0, "count must be >= 0"
1150 self.print(NewLine(count))
1151
1152 def clear(self, home: bool = True) -> None:
1153 """Clear the screen.
1154
1155 Args:
1156 home (bool, optional): Also move the cursor to 'home' position. Defaults to True.
1157 """
1158 if home:
1159 self.control(Control.clear(), Control.home())
1160 else:
1161 self.control(Control.clear())
1162
1163 def status(
1164 self,
1165 status: RenderableType,
1166 *,
1167 spinner: str = "dots",
1168 spinner_style: StyleType = "status.spinner",
1169 speed: float = 1.0,
1170 refresh_per_second: float = 12.5,
1171 ) -> "Status":
1172 """Display a status and spinner.
1173
1174 Args:
1175 status (RenderableType): A status renderable (str or Text typically).
1176 spinner (str, optional): Name of spinner animation (see python -m rich.spinner). Defaults to "dots".
1177 spinner_style (StyleType, optional): Style of spinner. Defaults to "status.spinner".
1178 speed (float, optional): Speed factor for spinner animation. Defaults to 1.0.
1179 refresh_per_second (float, optional): Number of refreshes per second. Defaults to 12.5.
1180
1181 Returns:
1182 Status: A Status object that may be used as a context manager.
1183 """
1184 from .status import Status
1185
1186 status_renderable = Status(
1187 status,
1188 console=self,
1189 spinner=spinner,
1190 spinner_style=spinner_style,
1191 speed=speed,
1192 refresh_per_second=refresh_per_second,
1193 )
1194 return status_renderable
1195
1196 def show_cursor(self, show: bool = True) -> bool:
1197 """Show or hide the cursor.
1198
1199 Args:
1200 show (bool, optional): Set visibility of the cursor.
1201 """
1202 if self.is_terminal:
1203 self.control(Control.show_cursor(show))
1204 return True
1205 return False
1206
1207 def set_alt_screen(self, enable: bool = True) -> bool:
1208 """Enables alternative screen mode.
1209
1210 Note, if you enable this mode, you should ensure that is disabled before
1211 the application exits. See :meth:`~rich.Console.screen` for a context manager
1212 that handles this for you.
1213
1214 Args:
1215 enable (bool, optional): Enable (True) or disable (False) alternate screen. Defaults to True.
1216
1217 Returns:
1218 bool: True if the control codes were written.
1219
1220 """
1221 changed = False
1222 if self.is_terminal and not self.legacy_windows:
1223 self.control(Control.alt_screen(enable))
1224 changed = True
1225 self._is_alt_screen = enable
1226 return changed
1227
1228 @property
1229 def is_alt_screen(self) -> bool:
1230 """Check if the alt screen was enabled.
1231
1232 Returns:
1233 bool: True if the alt screen was enabled, otherwise False.
1234 """
1235 return self._is_alt_screen
1236
1237 def set_window_title(self, title: str) -> bool:
1238 """Set the title of the console terminal window.
1239
1240 Warning: There is no means within Rich of "resetting" the window title to its
1241 previous value, meaning the title you set will persist even after your application
1242 exits.
1243
1244 ``fish`` shell resets the window title before and after each command by default,
1245 negating this issue. Windows Terminal and command prompt will also reset the title for you.
1246 Most other shells and terminals, however, do not do this.
1247
1248 Some terminals may require configuration changes before you can set the title.
1249 Some terminals may not support setting the title at all.
1250
1251 Other software (including the terminal itself, the shell, custom prompts, plugins, etc.)
1252 may also set the terminal window title. This could result in whatever value you write
1253 using this method being overwritten.
1254
1255 Args:
1256 title (str): The new title of the terminal window.
1257
1258 Returns:
1259 bool: True if the control code to change the terminal title was
1260 written, otherwise False. Note that a return value of True
1261 does not guarantee that the window title has actually changed,
1262 since the feature may be unsupported/disabled in some terminals.
1263 """
1264 if self.is_terminal:
1265 self.control(Control.title(title))
1266 return True
1267 return False
1268
1269 def screen(
1270 self, hide_cursor: bool = True, style: Optional[StyleType] = None
1271 ) -> "ScreenContext":
1272 """Context manager to enable and disable 'alternative screen' mode.
1273
1274 Args:
1275 hide_cursor (bool, optional): Also hide the cursor. Defaults to False.
1276 style (Style, optional): Optional style for screen. Defaults to None.
1277
1278 Returns:
1279 ~ScreenContext: Context which enables alternate screen on enter, and disables it on exit.
1280 """
1281 return ScreenContext(self, hide_cursor=hide_cursor, style=style or "")
1282
1283 def measure(
1284 self, renderable: RenderableType, *, options: Optional[ConsoleOptions] = None
1285 ) -> Measurement:
1286 """Measure a renderable. Returns a :class:`~rich.measure.Measurement` object which contains
1287 information regarding the number of characters required to print the renderable.
1288
1289 Args:
1290 renderable (RenderableType): Any renderable or string.
1291 options (Optional[ConsoleOptions], optional): Options to use when measuring, or None
1292 to use default options. Defaults to None.
1293
1294 Returns:
1295 Measurement: A measurement of the renderable.
1296 """
1297 measurement = Measurement.get(self, options or self.options, renderable)
1298 return measurement
1299
1300 def render(
1301 self, renderable: RenderableType, options: Optional[ConsoleOptions] = None
1302 ) -> Iterable[Segment]:
1303 """Render an object in to an iterable of `Segment` instances.
1304
1305 This method contains the logic for rendering objects with the console protocol.
1306 You are unlikely to need to use it directly, unless you are extending the library.
1307
1308 Args:
1309 renderable (RenderableType): An object supporting the console protocol, or
1310 an object that may be converted to a string.
1311 options (ConsoleOptions, optional): An options object, or None to use self.options. Defaults to None.
1312
1313 Returns:
1314 Iterable[Segment]: An iterable of segments that may be rendered.
1315 """
1316
1317 _options = options or self.options
1318 if _options.max_width < 1:
1319 # No space to render anything. This prevents potential recursion errors.
1320 return
1321 render_iterable: RenderResult
1322
1323 renderable = rich_cast(renderable)
1324 if hasattr(renderable, "__rich_console__") and not isclass(renderable):
1325 render_iterable = renderable.__rich_console__(self, _options)
1326 elif isinstance(renderable, str):
1327 text_renderable = self.render_str(
1328 renderable, highlight=_options.highlight, markup=_options.markup
1329 )
1330 render_iterable = text_renderable.__rich_console__(self, _options)
1331 else:
1332 raise errors.NotRenderableError(
1333 f"Unable to render {renderable!r}; "
1334 "A str, Segment or object with __rich_console__ method is required"
1335 )
1336
1337 try:
1338 iter_render = iter(render_iterable)
1339 except TypeError:
1340 raise errors.NotRenderableError(
1341 f"object {render_iterable!r} is not renderable"
1342 )
1343 _Segment = Segment
1344 _options = _options.reset_height()
1345 for render_output in iter_render:
1346 if isinstance(render_output, _Segment):
1347 yield render_output
1348 else:
1349 yield from self.render(render_output, _options)
1350
1351 def render_lines(
1352 self,
1353 renderable: RenderableType,
1354 options: Optional[ConsoleOptions] = None,
1355 *,
1356 style: Optional[Style] = None,
1357 pad: bool = True,
1358 new_lines: bool = False,
1359 ) -> List[List[Segment]]:
1360 """Render objects in to a list of lines.
1361
1362 The output of render_lines is useful when further formatting of rendered console text
1363 is required, such as the Panel class which draws a border around any renderable object.
1364
1365 Args:
1366 renderable (RenderableType): Any object renderable in the console.
1367 options (Optional[ConsoleOptions], optional): Console options, or None to use self.options. Default to ``None``.
1368 style (Style, optional): Optional style to apply to renderables. Defaults to ``None``.
1369 pad (bool, optional): Pad lines shorter than render width. Defaults to ``True``.
1370 new_lines (bool, optional): Include "\n" characters at end of lines.
1371
1372 Returns:
1373 List[List[Segment]]: A list of lines, where a line is a list of Segment objects.
1374 """
1375 with self._lock:
1376 render_options = options or self.options
1377 _rendered = self.render(renderable, render_options)
1378 if style:
1379 _rendered = Segment.apply_style(_rendered, style)
1380
1381 render_height = render_options.height
1382 if render_height is not None:
1383 render_height = max(0, render_height)
1384
1385 lines = list(
1386 islice(
1387 Segment.split_and_crop_lines(
1388 _rendered,
1389 render_options.max_width,
1390 include_new_lines=new_lines,
1391 pad=pad,
1392 style=style,
1393 ),
1394 None,
1395 render_height,
1396 )
1397 )
1398 if render_options.height is not None:
1399 extra_lines = render_options.height - len(lines)
1400 if extra_lines > 0:
1401 pad_line = [
1402 (
1403 [
1404 Segment(" " * render_options.max_width, style),
1405 Segment("\n"),
1406 ]
1407 if new_lines
1408 else [Segment(" " * render_options.max_width, style)]
1409 )
1410 ]
1411 lines.extend(pad_line * extra_lines)
1412
1413 return lines
1414
1415 def render_str(
1416 self,
1417 text: str,
1418 *,
1419 style: Union[str, Style] = "",
1420 justify: Optional[JustifyMethod] = None,
1421 overflow: Optional[OverflowMethod] = None,
1422 emoji: Optional[bool] = None,
1423 markup: Optional[bool] = None,
1424 highlight: Optional[bool] = None,
1425 highlighter: Optional[HighlighterType] = None,
1426 ) -> "Text":
1427 """Convert a string to a Text instance. This is called automatically if
1428 you print or log a string.
1429
1430 Args:
1431 text (str): Text to render.
1432 style (Union[str, Style], optional): Style to apply to rendered text.
1433 justify (str, optional): Justify method: "default", "left", "center", "full", or "right". Defaults to ``None``.
1434 overflow (str, optional): Overflow method: "crop", "fold", or "ellipsis". Defaults to ``None``.
1435 emoji (Optional[bool], optional): Enable emoji, or ``None`` to use Console default.
1436 markup (Optional[bool], optional): Enable markup, or ``None`` to use Console default.
1437 highlight (Optional[bool], optional): Enable highlighting, or ``None`` to use Console default.
1438 highlighter (HighlighterType, optional): Optional highlighter to apply.
1439 Returns:
1440 ConsoleRenderable: Renderable object.
1441
1442 """
1443 emoji_enabled = emoji or (emoji is None and self._emoji)
1444 markup_enabled = markup or (markup is None and self._markup)
1445 highlight_enabled = highlight or (highlight is None and self._highlight)
1446
1447 if markup_enabled:
1448 rich_text = render_markup(
1449 text,
1450 style=style,
1451 emoji=emoji_enabled,
1452 emoji_variant=self._emoji_variant,
1453 )
1454 rich_text.justify = justify
1455 rich_text.overflow = overflow
1456 else:
1457 rich_text = Text(
1458 (
1459 _emoji_replace(text, default_variant=self._emoji_variant)
1460 if emoji_enabled
1461 else text
1462 ),
1463 justify=justify,
1464 overflow=overflow,
1465 style=style,
1466 )
1467
1468 _highlighter = (highlighter or self.highlighter) if highlight_enabled else None
1469 if _highlighter is not None:
1470 highlight_text = _highlighter(str(rich_text))
1471 highlight_text.copy_styles(rich_text)
1472 return highlight_text
1473
1474 return rich_text
1475
1476 def get_style(
1477 self, name: Union[str, Style], *, default: Optional[Union[Style, str]] = None
1478 ) -> Style:
1479 """Get a Style instance by its theme name or parse a definition.
1480
1481 Args:
1482 name (str): The name of a style or a style definition.
1483
1484 Returns:
1485 Style: A Style object.
1486
1487 Raises:
1488 MissingStyle: If no style could be parsed from name.
1489
1490 """
1491 if isinstance(name, Style):
1492 return name
1493
1494 try:
1495 style = self._theme_stack.get(name)
1496 if style is None:
1497 style = Style.parse(name)
1498 return style.copy() if style.link else style
1499 except errors.StyleSyntaxError as error:
1500 if default is not None:
1501 return self.get_style(default)
1502 raise errors.MissingStyle(
1503 f"Failed to get style {name!r}; {error}"
1504 ) from None
1505
1506 def _collect_renderables(
1507 self,
1508 objects: Iterable[Any],
1509 sep: str,
1510 end: str,
1511 *,
1512 justify: Optional[JustifyMethod] = None,
1513 emoji: Optional[bool] = None,
1514 markup: Optional[bool] = None,
1515 highlight: Optional[bool] = None,
1516 ) -> List[ConsoleRenderable]:
1517 """Combine a number of renderables and text into one renderable.
1518
1519 Args:
1520 objects (Iterable[Any]): Anything that Rich can render.
1521 sep (str): String to write between print data.
1522 end (str): String to write at end of print data.
1523 justify (str, optional): One of "left", "right", "center", or "full". Defaults to ``None``.
1524 emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default.
1525 markup (Optional[bool], optional): Enable markup, or ``None`` to use console default.
1526 highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use console default.
1527
1528 Returns:
1529 List[ConsoleRenderable]: A list of things to render.
1530 """
1531 renderables: List[ConsoleRenderable] = []
1532 _append = renderables.append
1533 text: List[Text] = []
1534 append_text = text.append
1535
1536 append = _append
1537 if justify in ("left", "center", "right"):
1538
1539 def align_append(renderable: RenderableType) -> None:
1540 _append(Align(renderable, cast(AlignMethod, justify)))
1541
1542 append = align_append
1543
1544 _highlighter: HighlighterType = _null_highlighter
1545 if highlight or (highlight is None and self._highlight):
1546 _highlighter = self.highlighter
1547
1548 def check_text() -> None:
1549 if text:
1550 sep_text = Text(sep, justify=justify, end=end)
1551 append(sep_text.join(text))
1552 text.clear()
1553
1554 for renderable in objects:
1555 renderable = rich_cast(renderable)
1556 if isinstance(renderable, str):
1557 append_text(
1558 self.render_str(
1559 renderable,
1560 emoji=emoji,
1561 markup=markup,
1562 highlight=highlight,
1563 highlighter=_highlighter,
1564 )
1565 )
1566 elif isinstance(renderable, Text):
1567 append_text(renderable)
1568 elif isinstance(renderable, ConsoleRenderable):
1569 check_text()
1570 append(renderable)
1571 elif is_expandable(renderable):
1572 check_text()
1573 append(Pretty(renderable, highlighter=_highlighter))
1574 else:
1575 append_text(_highlighter(str(renderable)))
1576
1577 check_text()
1578
1579 if self.style is not None:
1580 style = self.get_style(self.style)
1581 renderables = [Styled(renderable, style) for renderable in renderables]
1582
1583 return renderables
1584
1585 def rule(
1586 self,
1587 title: TextType = "",
1588 *,
1589 characters: str = "─",
1590 style: Union[str, Style] = "rule.line",
1591 align: AlignMethod = "center",
1592 ) -> None:
1593 """Draw a line with optional centered title.
1594
1595 Args:
1596 title (str, optional): Text to render over the rule. Defaults to "".
1597 characters (str, optional): Character(s) to form the line. Defaults to "─".
1598 style (str, optional): Style of line. Defaults to "rule.line".
1599 align (str, optional): How to align the title, one of "left", "center", or "right". Defaults to "center".
1600 """
1601 from .rule import Rule
1602
1603 rule = Rule(title=title, characters=characters, style=style, align=align)
1604 self.print(rule)
1605
1606 def control(self, *control: Control) -> None:
1607 """Insert non-printing control codes.
1608
1609 Args:
1610 control_codes (str): Control codes, such as those that may move the cursor.
1611 """
1612 if not self.is_dumb_terminal:
1613 with self:
1614 self._buffer.extend(_control.segment for _control in control)
1615
1616 def out(
1617 self,
1618 *objects: Any,
1619 sep: str = " ",
1620 end: str = "\n",
1621 style: Optional[Union[str, Style]] = None,
1622 highlight: Optional[bool] = None,
1623 ) -> None:
1624 """Output to the terminal. This is a low-level way of writing to the terminal which unlike
1625 :meth:`~rich.console.Console.print` won't pretty print, wrap text, or apply markup, but will
1626 optionally apply highlighting and a basic style.
1627
1628 Args:
1629 sep (str, optional): String to write between print data. Defaults to " ".
1630 end (str, optional): String to write at end of print data. Defaults to "\\\\n".
1631 style (Union[str, Style], optional): A style to apply to output. Defaults to None.
1632 highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use
1633 console default. Defaults to ``None``.
1634 """
1635 raw_output: str = sep.join(str(_object) for _object in objects)
1636 self.print(
1637 raw_output,
1638 style=style,
1639 highlight=highlight,
1640 emoji=False,
1641 markup=False,
1642 no_wrap=True,
1643 overflow="ignore",
1644 crop=False,
1645 end=end,
1646 )
1647
1648 def print(
1649 self,
1650 *objects: Any,
1651 sep: str = " ",
1652 end: str = "\n",
1653 style: Optional[Union[str, Style]] = None,
1654 justify: Optional[JustifyMethod] = None,
1655 overflow: Optional[OverflowMethod] = None,
1656 no_wrap: Optional[bool] = None,
1657 emoji: Optional[bool] = None,
1658 markup: Optional[bool] = None,
1659 highlight: Optional[bool] = None,
1660 width: Optional[int] = None,
1661 height: Optional[int] = None,
1662 crop: bool = True,
1663 soft_wrap: Optional[bool] = None,
1664 new_line_start: bool = False,
1665 ) -> None:
1666 """Print to the console.
1667
1668 Args:
1669 objects (positional args): Objects to log to the terminal.
1670 sep (str, optional): String to write between print data. Defaults to " ".
1671 end (str, optional): String to write at end of print data. Defaults to "\\\\n".
1672 style (Union[str, Style], optional): A style to apply to output. Defaults to None.
1673 justify (str, optional): Justify method: "default", "left", "right", "center", or "full". Defaults to ``None``.
1674 overflow (str, optional): Overflow method: "ignore", "crop", "fold", or "ellipsis". Defaults to None.
1675 no_wrap (Optional[bool], optional): Disable word wrapping. Defaults to None.
1676 emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default. Defaults to ``None``.
1677 markup (Optional[bool], optional): Enable markup, or ``None`` to use console default. Defaults to ``None``.
1678 highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use console default. Defaults to ``None``.
1679 width (Optional[int], optional): Width of output, or ``None`` to auto-detect. Defaults to ``None``.
1680 crop (Optional[bool], optional): Crop output to width of terminal. Defaults to True.
1681 soft_wrap (bool, optional): Enable soft wrap mode which disables word wrapping and cropping of text or ``None`` for
1682 Console default. Defaults to ``None``.
1683 new_line_start (bool, False): Insert a new line at the start if the output contains more than one line. Defaults to ``False``.
1684 """
1685 if not objects:
1686 objects = (NewLine(),)
1687
1688 if soft_wrap is None:
1689 soft_wrap = self.soft_wrap
1690 if soft_wrap:
1691 if no_wrap is None:
1692 no_wrap = True
1693 if overflow is None:
1694 overflow = "ignore"
1695 crop = False
1696 render_hooks = self._render_hooks[:]
1697 with self:
1698 renderables = self._collect_renderables(
1699 objects,
1700 sep,
1701 end,
1702 justify=justify,
1703 emoji=emoji,
1704 markup=markup,
1705 highlight=highlight,
1706 )
1707 for hook in render_hooks:
1708 renderables = hook.process_renderables(renderables)
1709 render_options = self.options.update(
1710 justify=justify,
1711 overflow=overflow,
1712 width=min(width, self.width) if width is not None else NO_CHANGE,
1713 height=height,
1714 no_wrap=no_wrap,
1715 markup=markup,
1716 highlight=highlight,
1717 )
1718
1719 new_segments: List[Segment] = []
1720 extend = new_segments.extend
1721 render = self.render
1722 if style is None:
1723 for renderable in renderables:
1724 extend(render(renderable, render_options))
1725 else:
1726 for renderable in renderables:
1727 extend(
1728 Segment.apply_style(
1729 render(renderable, render_options), self.get_style(style)
1730 )
1731 )
1732 if new_line_start:
1733 if (
1734 len("".join(segment.text for segment in new_segments).splitlines())
1735 > 1
1736 ):
1737 new_segments.insert(0, Segment.line())
1738 if crop:
1739 buffer_extend = self._buffer.extend
1740 for line in Segment.split_and_crop_lines(
1741 new_segments, self.width, pad=False
1742 ):
1743 buffer_extend(line)
1744 else:
1745 self._buffer.extend(new_segments)
1746
1747 def print_json(
1748 self,
1749 json: Optional[str] = None,
1750 *,
1751 data: Any = None,
1752 indent: Union[None, int, str] = 2,
1753 highlight: bool = True,
1754 skip_keys: bool = False,
1755 ensure_ascii: bool = False,
1756 check_circular: bool = True,
1757 allow_nan: bool = True,
1758 default: Optional[Callable[[Any], Any]] = None,
1759 sort_keys: bool = False,
1760 ) -> None:
1761 """Pretty prints JSON. Output will be valid JSON.
1762
1763 Args:
1764 json (Optional[str]): A string containing JSON.
1765 data (Any): If json is not supplied, then encode this data.
1766 indent (Union[None, int, str], optional): Number of spaces to indent. Defaults to 2.
1767 highlight (bool, optional): Enable highlighting of output: Defaults to True.
1768 skip_keys (bool, optional): Skip keys not of a basic type. Defaults to False.
1769 ensure_ascii (bool, optional): Escape all non-ascii characters. Defaults to False.
1770 check_circular (bool, optional): Check for circular references. Defaults to True.
1771 allow_nan (bool, optional): Allow NaN and Infinity values. Defaults to True.
1772 default (Callable, optional): A callable that converts values that can not be encoded
1773 in to something that can be JSON encoded. Defaults to None.
1774 sort_keys (bool, optional): Sort dictionary keys. Defaults to False.
1775 """
1776 from pip._vendor.rich.json import JSON
1777
1778 if json is None:
1779 json_renderable = JSON.from_data(
1780 data,
1781 indent=indent,
1782 highlight=highlight,
1783 skip_keys=skip_keys,
1784 ensure_ascii=ensure_ascii,
1785 check_circular=check_circular,
1786 allow_nan=allow_nan,
1787 default=default,
1788 sort_keys=sort_keys,
1789 )
1790 else:
1791 if not isinstance(json, str):
1792 raise TypeError(
1793 f"json must be str. Did you mean print_json(data={json!r}) ?"
1794 )
1795 json_renderable = JSON(
1796 json,
1797 indent=indent,
1798 highlight=highlight,
1799 skip_keys=skip_keys,
1800 ensure_ascii=ensure_ascii,
1801 check_circular=check_circular,
1802 allow_nan=allow_nan,
1803 default=default,
1804 sort_keys=sort_keys,
1805 )
1806 self.print(json_renderable, soft_wrap=True)
1807
1808 def update_screen(
1809 self,
1810 renderable: RenderableType,
1811 *,
1812 region: Optional[Region] = None,
1813 options: Optional[ConsoleOptions] = None,
1814 ) -> None:
1815 """Update the screen at a given offset.
1816
1817 Args:
1818 renderable (RenderableType): A Rich renderable.
1819 region (Region, optional): Region of screen to update, or None for entire screen. Defaults to None.
1820 x (int, optional): x offset. Defaults to 0.
1821 y (int, optional): y offset. Defaults to 0.
1822
1823 Raises:
1824 errors.NoAltScreen: If the Console isn't in alt screen mode.
1825
1826 """
1827 if not self.is_alt_screen:
1828 raise errors.NoAltScreen("Alt screen must be enabled to call update_screen")
1829 render_options = options or self.options
1830 if region is None:
1831 x = y = 0
1832 render_options = render_options.update_dimensions(
1833 render_options.max_width, render_options.height or self.height
1834 )
1835 else:
1836 x, y, width, height = region
1837 render_options = render_options.update_dimensions(width, height)
1838
1839 lines = self.render_lines(renderable, options=render_options)
1840 self.update_screen_lines(lines, x, y)
1841
1842 def update_screen_lines(
1843 self, lines: List[List[Segment]], x: int = 0, y: int = 0
1844 ) -> None:
1845 """Update lines of the screen at a given offset.
1846
1847 Args:
1848 lines (List[List[Segment]]): Rendered lines (as produced by :meth:`~rich.Console.render_lines`).
1849 x (int, optional): x offset (column no). Defaults to 0.
1850 y (int, optional): y offset (column no). Defaults to 0.
1851
1852 Raises:
1853 errors.NoAltScreen: If the Console isn't in alt screen mode.
1854 """
1855 if not self.is_alt_screen:
1856 raise errors.NoAltScreen("Alt screen must be enabled to call update_screen")
1857 screen_update = ScreenUpdate(lines, x, y)
1858 segments = self.render(screen_update)
1859 self._buffer.extend(segments)
1860 self._check_buffer()
1861
1862 def print_exception(
1863 self,
1864 *,
1865 width: Optional[int] = 100,
1866 extra_lines: int = 3,
1867 theme: Optional[str] = None,
1868 word_wrap: bool = False,
1869 show_locals: bool = False,
1870 suppress: Iterable[Union[str, ModuleType]] = (),
1871 max_frames: int = 100,
1872 ) -> None:
1873 """Prints a rich render of the last exception and traceback.
1874
1875 Args:
1876 width (Optional[int], optional): Number of characters used to render code. Defaults to 100.
1877 extra_lines (int, optional): Additional lines of code to render. Defaults to 3.
1878 theme (str, optional): Override pygments theme used in traceback
1879 word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False.
1880 show_locals (bool, optional): Enable display of local variables. Defaults to False.
1881 suppress (Iterable[Union[str, ModuleType]]): Optional sequence of modules or paths to exclude from traceback.
1882 max_frames (int): Maximum number of frames to show in a traceback, 0 for no maximum. Defaults to 100.
1883 """
1884 from .traceback import Traceback
1885
1886 traceback = Traceback(
1887 width=width,
1888 extra_lines=extra_lines,
1889 theme=theme,
1890 word_wrap=word_wrap,
1891 show_locals=show_locals,
1892 suppress=suppress,
1893 max_frames=max_frames,
1894 )
1895 self.print(traceback)
1896
1897 @staticmethod
1898 def _caller_frame_info(
1899 offset: int,
1900 currentframe: Callable[[], Optional[FrameType]] = inspect.currentframe,
1901 ) -> Tuple[str, int, Dict[str, Any]]:
1902 """Get caller frame information.
1903
1904 Args:
1905 offset (int): the caller offset within the current frame stack.
1906 currentframe (Callable[[], Optional[FrameType]], optional): the callable to use to
1907 retrieve the current frame. Defaults to ``inspect.currentframe``.
1908
1909 Returns:
1910 Tuple[str, int, Dict[str, Any]]: A tuple containing the filename, the line number and
1911 the dictionary of local variables associated with the caller frame.
1912
1913 Raises:
1914 RuntimeError: If the stack offset is invalid.
1915 """
1916 # Ignore the frame of this local helper
1917 offset += 1
1918
1919 frame = currentframe()
1920 if frame is not None:
1921 # Use the faster currentframe where implemented
1922 while offset and frame is not None:
1923 frame = frame.f_back
1924 offset -= 1
1925 assert frame is not None
1926 return frame.f_code.co_filename, frame.f_lineno, frame.f_locals
1927 else:
1928 # Fallback to the slower stack
1929 frame_info = inspect.stack()[offset]
1930 return frame_info.filename, frame_info.lineno, frame_info.frame.f_locals
1931
1932 def log(
1933 self,
1934 *objects: Any,
1935 sep: str = " ",
1936 end: str = "\n",
1937 style: Optional[Union[str, Style]] = None,
1938 justify: Optional[JustifyMethod] = None,
1939 emoji: Optional[bool] = None,
1940 markup: Optional[bool] = None,
1941 highlight: Optional[bool] = None,
1942 log_locals: bool = False,
1943 _stack_offset: int = 1,
1944 ) -> None:
1945 """Log rich content to the terminal.
1946
1947 Args:
1948 objects (positional args): Objects to log to the terminal.
1949 sep (str, optional): String to write between print data. Defaults to " ".
1950 end (str, optional): String to write at end of print data. Defaults to "\\\\n".
1951 style (Union[str, Style], optional): A style to apply to output. Defaults to None.
1952 justify (str, optional): One of "left", "right", "center", or "full". Defaults to ``None``.
1953 emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default. Defaults to None.
1954 markup (Optional[bool], optional): Enable markup, or ``None`` to use console default. Defaults to None.
1955 highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use console default. Defaults to None.
1956 log_locals (bool, optional): Boolean to enable logging of locals where ``log()``
1957 was called. Defaults to False.
1958 _stack_offset (int, optional): Offset of caller from end of call stack. Defaults to 1.
1959 """
1960 if not objects:
1961 objects = (NewLine(),)
1962
1963 render_hooks = self._render_hooks[:]
1964
1965 with self:
1966 renderables = self._collect_renderables(
1967 objects,
1968 sep,
1969 end,
1970 justify=justify,
1971 emoji=emoji,
1972 markup=markup,
1973 highlight=highlight,
1974 )
1975 if style is not None:
1976 renderables = [Styled(renderable, style) for renderable in renderables]
1977
1978 filename, line_no, locals = self._caller_frame_info(_stack_offset)
1979 link_path = None if filename.startswith("<") else os.path.abspath(filename)
1980 path = filename.rpartition(os.sep)[-1]
1981 if log_locals:
1982 locals_map = {
1983 key: value
1984 for key, value in locals.items()
1985 if not key.startswith("__")
1986 }
1987 renderables.append(render_scope(locals_map, title="[i]locals"))
1988
1989 renderables = [
1990 self._log_render(
1991 self,
1992 renderables,
1993 log_time=self.get_datetime(),
1994 path=path,
1995 line_no=line_no,
1996 link_path=link_path,
1997 )
1998 ]
1999 for hook in render_hooks:
2000 renderables = hook.process_renderables(renderables)
2001 new_segments: List[Segment] = []
2002 extend = new_segments.extend
2003 render = self.render
2004 render_options = self.options
2005 for renderable in renderables:
2006 extend(render(renderable, render_options))
2007 buffer_extend = self._buffer.extend
2008 for line in Segment.split_and_crop_lines(
2009 new_segments, self.width, pad=False
2010 ):
2011 buffer_extend(line)
2012
2013 def on_broken_pipe(self) -> None:
2014 """This function is called when a `BrokenPipeError` is raised.
2015
2016 This can occur when piping Textual output in Linux and macOS.
2017 The default implementation is to exit the app, but you could implement
2018 this method in a subclass to change the behavior.
2019
2020 See https://docs.python.org/3/library/signal.html#note-on-sigpipe for details.
2021 """
2022 self.quiet = True
2023 devnull = os.open(os.devnull, os.O_WRONLY)
2024 os.dup2(devnull, sys.stdout.fileno())
2025 raise SystemExit(1)
2026
2027 def _check_buffer(self) -> None:
2028 """Check if the buffer may be rendered. Render it if it can (e.g. Console.quiet is False)
2029 Rendering is supported on Windows, Unix and Jupyter environments. For
2030 legacy Windows consoles, the win32 API is called directly.
2031 This method will also record what it renders if recording is enabled via Console.record.
2032 """
2033 if self.quiet:
2034 del self._buffer[:]
2035 return
2036
2037 try:
2038 self._write_buffer()
2039 except BrokenPipeError:
2040 self.on_broken_pipe()
2041
2042 def _write_buffer(self) -> None:
2043 """Write the buffer to the output file."""
2044
2045 with self._lock:
2046 if self.record and not self._buffer_index:
2047 with self._record_buffer_lock:
2048 self._record_buffer.extend(self._buffer[:])
2049
2050 if self._buffer_index == 0:
2051 if self.is_jupyter: # pragma: no cover
2052 from .jupyter import display
2053
2054 display(self._buffer, self._render_buffer(self._buffer[:]))
2055 del self._buffer[:]
2056 else:
2057 if WINDOWS:
2058 use_legacy_windows_render = False
2059 if self.legacy_windows:
2060 fileno = get_fileno(self.file)
2061 if fileno is not None:
2062 use_legacy_windows_render = (
2063 fileno in _STD_STREAMS_OUTPUT
2064 )
2065
2066 if use_legacy_windows_render:
2067 from pip._vendor.rich._win32_console import LegacyWindowsTerm
2068 from pip._vendor.rich._windows_renderer import legacy_windows_render
2069
2070 buffer = self._buffer[:]
2071 if self.no_color and self._color_system:
2072 buffer = list(Segment.remove_color(buffer))
2073
2074 legacy_windows_render(buffer, LegacyWindowsTerm(self.file))
2075 else:
2076 # Either a non-std stream on legacy Windows, or modern Windows.
2077 text = self._render_buffer(self._buffer[:])
2078 # https://bugs.python.org/issue37871
2079 # https://github.com/python/cpython/issues/82052
2080 # We need to avoid writing more than 32Kb in a single write, due to the above bug
2081 write = self.file.write
2082 # Worse case scenario, every character is 4 bytes of utf-8
2083 MAX_WRITE = 32 * 1024 // 4
2084 try:
2085 if len(text) <= MAX_WRITE:
2086 write(text)
2087 else:
2088 batch: List[str] = []
2089 batch_append = batch.append
2090 size = 0
2091 for line in text.splitlines(True):
2092 if size + len(line) > MAX_WRITE and batch:
2093 write("".join(batch))
2094 batch.clear()
2095 size = 0
2096 batch_append(line)
2097 size += len(line)
2098 if batch:
2099 write("".join(batch))
2100 batch.clear()
2101 except UnicodeEncodeError as error:
2102 error.reason = f"{error.reason}\n*** You may need to add PYTHONIOENCODING=utf-8 to your environment ***"
2103 raise
2104 else:
2105 text = self._render_buffer(self._buffer[:])
2106 try:
2107 self.file.write(text)
2108 except UnicodeEncodeError as error:
2109 error.reason = f"{error.reason}\n*** You may need to add PYTHONIOENCODING=utf-8 to your environment ***"
2110 raise
2111
2112 self.file.flush()
2113 del self._buffer[:]
2114
2115 def _render_buffer(self, buffer: Iterable[Segment]) -> str:
2116 """Render buffered output, and clear buffer."""
2117 output: List[str] = []
2118 append = output.append
2119 color_system = self._color_system
2120 legacy_windows = self.legacy_windows
2121 not_terminal = not self.is_terminal
2122 if self.no_color and color_system:
2123 buffer = Segment.remove_color(buffer)
2124 for text, style, control in buffer:
2125 if style:
2126 append(
2127 style.render(
2128 text,
2129 color_system=color_system,
2130 legacy_windows=legacy_windows,
2131 )
2132 )
2133 elif not (not_terminal and control):
2134 append(text)
2135
2136 rendered = "".join(output)
2137 return rendered
2138
2139 def input(
2140 self,
2141 prompt: TextType = "",
2142 *,
2143 markup: bool = True,
2144 emoji: bool = True,
2145 password: bool = False,
2146 stream: Optional[TextIO] = None,
2147 ) -> str:
2148 """Displays a prompt and waits for input from the user. The prompt may contain color / style.
2149
2150 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
2152 Args:
2153 prompt (Union[str, Text]): Text to render in the prompt.
2154 markup (bool, optional): Enable console markup (requires a str prompt). Defaults to True.
2155 emoji (bool, optional): Enable emoji (requires a str prompt). Defaults to True.
2156 password: (bool, optional): Hide typed text. Defaults to False.
2157 stream: (TextIO, optional): Optional file to read input from (rather than stdin). Defaults to None.
2158
2159 Returns:
2160 str: Text read from stdin.
2161 """
2162 if prompt:
2163 self.print(prompt, markup=markup, emoji=emoji, end="")
2164 if password:
2165 result = getpass("", stream=stream)
2166 else:
2167 if stream:
2168 result = stream.readline()
2169 else:
2170 result = input()
2171 return result
2172
2173 def export_text(self, *, clear: bool = True, styles: bool = False) -> str:
2174 """Generate text from console contents (requires record=True argument in constructor).
2175
2176 Args:
2177 clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``.
2178 styles (bool, optional): If ``True``, ansi escape codes will be included. ``False`` for plain text.
2179 Defaults to ``False``.
2180
2181 Returns:
2182 str: String containing console contents.
2183
2184 """
2185 assert (
2186 self.record
2187 ), "To export console contents set record=True in the constructor or instance"
2188
2189 with self._record_buffer_lock:
2190 if styles:
2191 text = "".join(
2192 (style.render(text) if style else text)
2193 for text, style, _ in self._record_buffer
2194 )
2195 else:
2196 text = "".join(
2197 segment.text
2198 for segment in self._record_buffer
2199 if not segment.control
2200 )
2201 if clear:
2202 del self._record_buffer[:]
2203 return text
2204
2205 def save_text(self, path: str, *, clear: bool = True, styles: bool = False) -> None:
2206 """Generate text from console and save to a given location (requires record=True argument in constructor).
2207
2208 Args:
2209 path (str): Path to write text files.
2210 clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``.
2211 styles (bool, optional): If ``True``, ansi style codes will be included. ``False`` for plain text.
2212 Defaults to ``False``.
2213
2214 """
2215 text = self.export_text(clear=clear, styles=styles)
2216 with open(path, "w", encoding="utf-8") as write_file:
2217 write_file.write(text)
2218
2219 def export_html(
2220 self,
2221 *,
2222 theme: Optional[TerminalTheme] = None,
2223 clear: bool = True,
2224 code_format: Optional[str] = None,
2225 inline_styles: bool = False,
2226 ) -> str:
2227 """Generate HTML from console contents (requires record=True argument in constructor).
2228
2229 Args:
2230 theme (TerminalTheme, optional): TerminalTheme object containing console colors.
2231 clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``.
2232 code_format (str, optional): Format string to render HTML. In addition to '{foreground}',
2233 '{background}', and '{code}', should contain '{stylesheet}' if inline_styles is ``False``.
2234 inline_styles (bool, optional): If ``True`` styles will be inlined in to spans, which makes files
2235 larger but easier to cut and paste markup. If ``False``, styles will be embedded in a style tag.
2236 Defaults to False.
2237
2238 Returns:
2239 str: String containing console contents as HTML.
2240 """
2241 assert (
2242 self.record
2243 ), "To export console contents set record=True in the constructor or instance"
2244 fragments: List[str] = []
2245 append = fragments.append
2246 _theme = theme or DEFAULT_TERMINAL_THEME
2247 stylesheet = ""
2248
2249 render_code_format = CONSOLE_HTML_FORMAT if code_format is None else code_format
2250
2251 with self._record_buffer_lock:
2252 if inline_styles:
2253 for text, style, _ in Segment.filter_control(
2254 Segment.simplify(self._record_buffer)
2255 ):
2256 text = escape(text)
2257 if style:
2258 rule = style.get_html_style(_theme)
2259 if style.link:
2260 text = f'<a href="{style.link}">{text}</a>'
2261 text = f'<span style="{rule}">{text}</span>' if rule else text
2262 append(text)
2263 else:
2264 styles: Dict[str, int] = {}
2265 for text, style, _ in Segment.filter_control(
2266 Segment.simplify(self._record_buffer)
2267 ):
2268 text = escape(text)
2269 if style:
2270 rule = style.get_html_style(_theme)
2271 style_number = styles.setdefault(rule, len(styles) + 1)
2272 if style.link:
2273 text = f'<a class="r{style_number}" href="{style.link}">{text}</a>'
2274 else:
2275 text = f'<span class="r{style_number}">{text}</span>'
2276 append(text)
2277 stylesheet_rules: List[str] = []
2278 stylesheet_append = stylesheet_rules.append
2279 for style_rule, style_number in styles.items():
2280 if style_rule:
2281 stylesheet_append(f".r{style_number} {{{style_rule}}}")
2282 stylesheet = "\n".join(stylesheet_rules)
2283
2284 rendered_code = render_code_format.format(
2285 code="".join(fragments),
2286 stylesheet=stylesheet,
2287 foreground=_theme.foreground_color.hex,
2288 background=_theme.background_color.hex,
2289 )
2290 if clear:
2291 del self._record_buffer[:]
2292 return rendered_code
2293
2294 def save_html(
2295 self,
2296 path: str,
2297 *,
2298 theme: Optional[TerminalTheme] = None,
2299 clear: bool = True,
2300 code_format: str = CONSOLE_HTML_FORMAT,
2301 inline_styles: bool = False,
2302 ) -> None:
2303 """Generate HTML from console contents and write to a file (requires record=True argument in constructor).
2304
2305 Args:
2306 path (str): Path to write html file.
2307 theme (TerminalTheme, optional): TerminalTheme object containing console colors.
2308 clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``.
2309 code_format (str, optional): Format string to render HTML. In addition to '{foreground}',
2310 '{background}', and '{code}', should contain '{stylesheet}' if inline_styles is ``False``.
2311 inline_styles (bool, optional): If ``True`` styles will be inlined in to spans, which makes files
2312 larger but easier to cut and paste markup. If ``False``, styles will be embedded in a style tag.
2313 Defaults to False.
2314
2315 """
2316 html = self.export_html(
2317 theme=theme,
2318 clear=clear,
2319 code_format=code_format,
2320 inline_styles=inline_styles,
2321 )
2322 with open(path, "w", encoding="utf-8") as write_file:
2323 write_file.write(html)
2324
2325 def export_svg(
2326 self,
2327 *,
2328 title: str = "Rich",
2329 theme: Optional[TerminalTheme] = None,
2330 clear: bool = True,
2331 code_format: str = CONSOLE_SVG_FORMAT,
2332 font_aspect_ratio: float = 0.61,
2333 unique_id: Optional[str] = None,
2334 ) -> str:
2335 """
2336 Generate an SVG from the console contents (requires record=True in Console constructor).
2337
2338 Args:
2339 title (str, optional): The title of the tab in the output image
2340 theme (TerminalTheme, optional): The ``TerminalTheme`` object to use to style the terminal
2341 clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``
2342 code_format (str, optional): Format string used to generate the SVG. Rich will inject a number of variables
2343 into the string in order to form the final SVG output. The default template used and the variables
2344 injected by Rich can be found by inspecting the ``console.CONSOLE_SVG_FORMAT`` variable.
2345 font_aspect_ratio (float, optional): The width to height ratio of the font used in the ``code_format``
2346 string. Defaults to 0.61, which is the width to height ratio of Fira Code (the default font).
2347 If you aren't specifying a different font inside ``code_format``, you probably don't need this.
2348 unique_id (str, optional): unique id that is used as the prefix for various elements (CSS styles, node
2349 ids). If not set, this defaults to a computed value based on the recorded content.
2350 """
2351
2352 from pip._vendor.rich.cells import cell_len
2353
2354 style_cache: Dict[Style, str] = {}
2355
2356 def get_svg_style(style: Style) -> str:
2357 """Convert a Style to CSS rules for SVG."""
2358 if style in style_cache:
2359 return style_cache[style]
2360 css_rules = []
2361 color = (
2362 _theme.foreground_color
2363 if (style.color is None or style.color.is_default)
2364 else style.color.get_truecolor(_theme)
2365 )
2366 bgcolor = (
2367 _theme.background_color
2368 if (style.bgcolor is None or style.bgcolor.is_default)
2369 else style.bgcolor.get_truecolor(_theme)
2370 )
2371 if style.reverse:
2372 color, bgcolor = bgcolor, color
2373 if style.dim:
2374 color = blend_rgb(color, bgcolor, 0.4)
2375 css_rules.append(f"fill: {color.hex}")
2376 if style.bold:
2377 css_rules.append("font-weight: bold")
2378 if style.italic:
2379 css_rules.append("font-style: italic;")
2380 if style.underline:
2381 css_rules.append("text-decoration: underline;")
2382 if style.strike:
2383 css_rules.append("text-decoration: line-through;")
2384
2385 css = ";".join(css_rules)
2386 style_cache[style] = css
2387 return css
2388
2389 _theme = theme or SVG_EXPORT_THEME
2390
2391 width = self.width
2392 char_height = 20
2393 char_width = char_height * font_aspect_ratio
2394 line_height = char_height * 1.22
2395
2396 margin_top = 1
2397 margin_right = 1
2398 margin_bottom = 1
2399 margin_left = 1
2400
2401 padding_top = 40
2402 padding_right = 8
2403 padding_bottom = 8
2404 padding_left = 8
2405
2406 padding_width = padding_left + padding_right
2407 padding_height = padding_top + padding_bottom
2408 margin_width = margin_left + margin_right
2409 margin_height = margin_top + margin_bottom
2410
2411 text_backgrounds: List[str] = []
2412 text_group: List[str] = []
2413 classes: Dict[str, int] = {}
2414 style_no = 1
2415
2416 def escape_text(text: str) -> str:
2417 """HTML escape text and replace spaces with nbsp."""
2418 return escape(text).replace(" ", " ")
2419
2420 def make_tag(
2421 name: str, content: Optional[str] = None, **attribs: object
2422 ) -> str:
2423 """Make a tag from name, content, and attributes."""
2424
2425 def stringify(value: object) -> str:
2426 if isinstance(value, (float)):
2427 return format(value, "g")
2428 return str(value)
2429
2430 tag_attribs = " ".join(
2431 f'{k.lstrip("_").replace("_", "-")}="{stringify(v)}"'
2432 for k, v in attribs.items()
2433 )
2434 return (
2435 f"<{name} {tag_attribs}>{content}</{name}>"
2436 if content
2437 else f"<{name} {tag_attribs}/>"
2438 )
2439
2440 with self._record_buffer_lock:
2441 segments = list(Segment.filter_control(self._record_buffer))
2442 if clear:
2443 self._record_buffer.clear()
2444
2445 if unique_id is None:
2446 unique_id = "terminal-" + str(
2447 zlib.adler32(
2448 ("".join(repr(segment) for segment in segments)).encode(
2449 "utf-8",
2450 "ignore",
2451 )
2452 + title.encode("utf-8", "ignore")
2453 )
2454 )
2455 y = 0
2456 for y, line in enumerate(Segment.split_and_crop_lines(segments, length=width)):
2457 x = 0
2458 for text, style, _control in line:
2459 style = style or Style()
2460 rules = get_svg_style(style)
2461 if rules not in classes:
2462 classes[rules] = style_no
2463 style_no += 1
2464 class_name = f"r{classes[rules]}"
2465
2466 if style.reverse:
2467 has_background = True
2468 background = (
2469 _theme.foreground_color.hex
2470 if style.color is None
2471 else style.color.get_truecolor(_theme).hex
2472 )
2473 else:
2474 bgcolor = style.bgcolor
2475 has_background = bgcolor is not None and not bgcolor.is_default
2476 background = (
2477 _theme.background_color.hex
2478 if style.bgcolor is None
2479 else style.bgcolor.get_truecolor(_theme).hex
2480 )
2481
2482 text_length = cell_len(text)
2483 if has_background:
2484 text_backgrounds.append(
2485 make_tag(
2486 "rect",
2487 fill=background,
2488 x=x * char_width,
2489 y=y * line_height + 1.5,
2490 width=char_width * text_length,
2491 height=line_height + 0.25,
2492 shape_rendering="crispEdges",
2493 )
2494 )
2495
2496 if text != " " * len(text):
2497 text_group.append(
2498 make_tag(
2499 "text",
2500 escape_text(text),
2501 _class=f"{unique_id}-{class_name}",
2502 x=x * char_width,
2503 y=y * line_height + char_height,
2504 textLength=char_width * len(text),
2505 clip_path=f"url(#{unique_id}-line-{y})",
2506 )
2507 )
2508 x += cell_len(text)
2509
2510 line_offsets = [line_no * line_height + 1.5 for line_no in range(y)]
2511 lines = "\n".join(
2512 f"""<clipPath id="{unique_id}-line-{line_no}">
2513 {make_tag("rect", x=0, y=offset, width=char_width * width, height=line_height + 0.25)}
2514 </clipPath>"""
2515 for line_no, offset in enumerate(line_offsets)
2516 )
2517
2518 styles = "\n".join(
2519 f".{unique_id}-r{rule_no} {{ {css} }}" for css, rule_no in classes.items()
2520 )
2521 backgrounds = "".join(text_backgrounds)
2522 matrix = "".join(text_group)
2523
2524 terminal_width = ceil(width * char_width + padding_width)
2525 terminal_height = (y + 1) * line_height + padding_height
2526 chrome = make_tag(
2527 "rect",
2528 fill=_theme.background_color.hex,
2529 stroke="rgba(255,255,255,0.35)",
2530 stroke_width="1",
2531 x=margin_left,
2532 y=margin_top,
2533 width=terminal_width,
2534 height=terminal_height,
2535 rx=8,
2536 )
2537
2538 title_color = _theme.foreground_color.hex
2539 if title:
2540 chrome += make_tag(
2541 "text",
2542 escape_text(title),
2543 _class=f"{unique_id}-title",
2544 fill=title_color,
2545 text_anchor="middle",
2546 x=terminal_width // 2,
2547 y=margin_top + char_height + 6,
2548 )
2549 chrome += f"""
2550 <g transform="translate(26,22)">
2551 <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
2552 <circle cx="22" cy="0" r="7" fill="#febc2e"/>
2553 <circle cx="44" cy="0" r="7" fill="#28c840"/>
2554 </g>
2555 """
2556
2557 svg = code_format.format(
2558 unique_id=unique_id,
2559 char_width=char_width,
2560 char_height=char_height,
2561 line_height=line_height,
2562 terminal_width=char_width * width - 1,
2563 terminal_height=(y + 1) * line_height - 1,
2564 width=terminal_width + margin_width,
2565 height=terminal_height + margin_height,
2566 terminal_x=margin_left + padding_left,
2567 terminal_y=margin_top + padding_top,
2568 styles=styles,
2569 chrome=chrome,
2570 backgrounds=backgrounds,
2571 matrix=matrix,
2572 lines=lines,
2573 )
2574 return svg
2575
2576 def save_svg(
2577 self,
2578 path: str,
2579 *,
2580 title: str = "Rich",
2581 theme: Optional[TerminalTheme] = None,
2582 clear: bool = True,
2583 code_format: str = CONSOLE_SVG_FORMAT,
2584 font_aspect_ratio: float = 0.61,
2585 unique_id: Optional[str] = None,
2586 ) -> None:
2587 """Generate an SVG file from the console contents (requires record=True in Console constructor).
2588
2589 Args:
2590 path (str): The path to write the SVG to.
2591 title (str, optional): The title of the tab in the output image
2592 theme (TerminalTheme, optional): The ``TerminalTheme`` object to use to style the terminal
2593 clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``
2594 code_format (str, optional): Format string used to generate the SVG. Rich will inject a number of variables
2595 into the string in order to form the final SVG output. The default template used and the variables
2596 injected by Rich can be found by inspecting the ``console.CONSOLE_SVG_FORMAT`` variable.
2597 font_aspect_ratio (float, optional): The width to height ratio of the font used in the ``code_format``
2598 string. Defaults to 0.61, which is the width to height ratio of Fira Code (the default font).
2599 If you aren't specifying a different font inside ``code_format``, you probably don't need this.
2600 unique_id (str, optional): unique id that is used as the prefix for various elements (CSS styles, node
2601 ids). If not set, this defaults to a computed value based on the recorded content.
2602 """
2603 svg = self.export_svg(
2604 title=title,
2605 theme=theme,
2606 clear=clear,
2607 code_format=code_format,
2608 font_aspect_ratio=font_aspect_ratio,
2609 unique_id=unique_id,
2610 )
2611 with open(path, "w", encoding="utf-8") as write_file:
2612 write_file.write(svg)
2613
2614
2615def _svg_hash(svg_main_code: str) -> str:
2616 """Returns a unique hash for the given SVG main code.
2617
2618 Args:
2619 svg_main_code (str): The content we're going to inject in the SVG envelope.
2620
2621 Returns:
2622 str: a hash of the given content
2623 """
2624 return str(zlib.adler32(svg_main_code.encode()))
2625
2626
2627if __name__ == "__main__": # pragma: no cover
2628 console = Console(record=True)
2629
2630 console.log(
2631 "JSONRPC [i]request[/i]",
2632 5,
2633 1.3,
2634 True,
2635 False,
2636 None,
2637 {
2638 "jsonrpc": "2.0",
2639 "method": "subtract",
2640 "params": {"minuend": 42, "subtrahend": 23},
2641 "id": 3,
2642 },
2643 )
2644
2645 console.log("Hello, World!", "{'a': 1}", repr(console))
2646
2647 console.print(
2648 {
2649 "name": None,
2650 "empty": [],
2651 "quiz": {
2652 "sport": {
2653 "answered": True,
2654 "q1": {
2655 "question": "Which one is correct team name in NBA?",
2656 "options": [
2657 "New York Bulls",
2658 "Los Angeles Kings",
2659 "Golden State Warriors",
2660 "Huston Rocket",
2661 ],
2662 "answer": "Huston Rocket",
2663 },
2664 },
2665 "maths": {
2666 "answered": False,
2667 "q1": {
2668 "question": "5 + 7 = ?",
2669 "options": [10, 11, 12, 13],
2670 "answer": 12,
2671 },
2672 "q2": {
2673 "question": "12 - 8 = ?",
2674 "options": [1, 2, 3, 4],
2675 "answer": 4,
2676 },
2677 },
2678 },
2679 }
2680 )