diff --git a/CHANGELOG.md b/CHANGELOG.md index 490857b35c..c5b1877289 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed extraction of recursive exceptions https://github.com/Textualize/rich/pull/3772 +### Added + +- Added `TTY_INTERACTIVE` environment variable to force interactive mode off or on https://github.com/Textualize/rich/pull/3777 + ## [14.0.0] - 2025-03-30 ### Added diff --git a/docs/source/console.rst b/docs/source/console.rst index c0d50ca18d..4d79886c24 100644 --- a/docs/source/console.rst +++ b/docs/source/console.rst @@ -405,13 +405,13 @@ If Rich detects that it is not writing to a terminal it will strip control codes Letting Rich auto-detect terminals is useful as it will write plain text when you pipe output to a file or other application. +.. _interactive_mode: + Interactive mode ---------------- -Rich will remove animations such as progress bars and status indicators when not writing to a terminal as you probably don't want to write these out to a text file (for example). You can override this behavior by setting the ``force_interactive`` argument on the constructor. Set it to True to enable animations or False to disable them. +Rich will remove animations such as progress bars and status indicators when not writing to a terminal as you probably don't want to write these out to a text file (for example). You can override this behavior by setting the ``force_interactive`` argument on the constructor. Set it to ``True`` to enable animations or ``False`` to disable them. -.. note:: - Some CI systems support ANSI color and style but not anything that moves the cursor or selectively refreshes parts of the terminal. For these you might want to set ``force_terminal`` to ``True`` and ``force_interactive`` to ``False``. Environment variables --------------------- @@ -429,8 +429,11 @@ If the environment variable ``NO_COLOR`` is set, Rich will disable all color in The environment variable ``TTY_COMPATIBLE`` is used to override Rich's auto-detection of terminal support. If ``TTY_COMPATIBLE`` is set to ``1`` then Rich will assume it is writing to a device which can handle escape sequences like a terminal. If ``TTY_COMPATIBLE`` is set to ``"0"``, then Rich will assume that it is writing to a device that is *not* capable of displaying escape sequences (such as a regular file). If the variable is not set, or set to a value other than "0" or "1", then Rich will attempt to auto-detect terminal support. +The environment variable ``TTY_INTERACTIVE`` is used to override Rich's auto-detection of :ref:`interactive_mode`. If you set this to ``"0"``, it will disable interactive mode even if Rich thinks it is writing to a terminal. Set this to ``"1"`` to force interactive mode on. If this environment variable is not set, or set to any other value, then interactive mode will be auto-detected as normal. + .. note:: - If you want Rich output in CI or Github Actions, then you should set ``TTY_COMPATIBLE=1``. + If you want Rich output in CI or Github Actions, then you should set ``TTY_COMPATIBLE=1`` and ``TTY_INTERACTIVE=0``. The combination of both these variables tells rich that it can output escape sequences, + and also that there is no user interacting with the terminal -- so it won't bother animating progress bars. If ``width`` / ``height`` arguments are not explicitly provided as arguments to ``Console`` then the environment variables ``COLUMNS`` / ``LINES`` can be used to set the console width / height. ``JUPYTER_COLUMNS`` / ``JUPYTER_LINES`` behave similarly and are used in Jupyter. diff --git a/rich/console.py b/rich/console.py index 5929fb1c56..7d4d4a8f64 100644 --- a/rich/console.py +++ b/rich/console.py @@ -22,18 +22,20 @@ Dict, Iterable, List, + Literal, Mapping, NamedTuple, Optional, + Protocol, TextIO, Tuple, Type, Union, cast, + runtime_checkable, ) from rich._null_file import NULL_FILE -from typing import Literal, Protocol, runtime_checkable from . import errors, themes from ._emoji_replace import _emoji_replace @@ -731,6 +733,14 @@ def __init__( if no_color is not None else self._environ.get("NO_COLOR", "") != "" ) + if force_interactive is None: + tty_interactive = self._environ.get("TTY_INTERACTIVE", None) + if tty_interactive is not None: + if tty_interactive == "0": + force_interactive = False + elif tty_interactive == "1": + force_interactive = True + self.is_interactive = ( (self.is_terminal and not self.is_dumb_terminal) if force_interactive is None diff --git a/rich/diagnose.py b/rich/diagnose.py index 5d163877da..9d5ff3ec32 100644 --- a/rich/diagnose.py +++ b/rich/diagnose.py @@ -26,6 +26,7 @@ def report() -> None: # pragma: no cover "TERM_PROGRAM", "TERM", "TTY_COMPATIBLE", + "TTY_INTERACTIVE", "VSCODE_VERBOSE_LOGGING", ) env = {name: os.getenv(name) for name in env_names} diff --git a/tests/test_console.py b/tests/test_console.py index 10576b2dae..499043b312 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -1038,6 +1038,31 @@ def test_capture_and_record() -> None: assert recorded_content == "Print 0\n" +def test_tty_interactive() -> None: + """Check TTY_INTERACTIVE environment var.""" + + # Bytes file, not interactive + console = Console(file=io.BytesIO()) + assert not console.is_interactive + + # Bytes file, force interactive + console = Console(file=io.BytesIO(), _environ={"TTY_INTERACTIVE": "1"}) + assert console.is_interactive + + # Force tty compatible, should be interactive + console = Console(file=io.BytesIO(), _environ={"TTY_COMPATIBLE": "1"}) + assert console.is_interactive + + # Force tty compatible, force not interactive + console = Console( + file=io.BytesIO(), _environ={"TTY_COMPATIBLE": "1", "TTY_INTERACTIVE": "0"} + ) + + # Bytes file, Unknown value of TTY_INTERACTIVE should still auto-detect + console = Console(file=io.BytesIO(), _environ={"TTY_INTERACTIVE": "foo"}) + assert not console.is_interactive + + def test_tty_compatible() -> None: """Check TTY_COMPATIBLE environment var."""