diff --git a/.coveragerc b/.coveragerc index ff1a046c08..2afac8dc60 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,7 +2,8 @@ omit = rich/jupyter.py rich/_windows.py rich/_timer.py - + rich/diagnose.py + [report] exclude_lines = pragma: no cover diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index fd623f58c2..7c92f61f11 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,5 +1,3 @@ # These are supported funding model platforms -github: willmcgugan -ko_fi: willmcgugan -tidelift: "pypi/rich" +ko_fi: textualize diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 05cd2cd112..6d6f45677f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -20,12 +20,21 @@ Provide a minimal code example that demonstrates the issue if you can. If the is What platform (Win/Linux/Mac) are you running on? What terminal software are you using? -I may ask you to cut and paste the output of the following commands. It may save some time if you do it now. +I may ask you to copy and paste the output of the following commands. It may save some time if you do it now. + +If you're using Rich in a terminal: ``` python -m rich.diagnose -python -m rich._windows pip freeze | grep rich ``` - + +If you're using Rich in a Jupyter Notebook, run the following snippet in a cell +and paste the output in your bug report. + +```python +from rich.diagnose import report +report() +``` + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index f6e942e5de..6e2ca38c29 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -18,7 +18,3 @@ Give as much detail as you can. Example code of how you would like it to work wo **What problem does it solve for you?** What problem do you have that this feature would solve? I may be able to suggest an existing way of solving it. - -**Did I help** - -If I was able to resolve your problem, consider [sponsoring](https://github.com/sponsors/willmcgugan) my work on Rich, or [buy me a coffee](https://ko-fi.com/willmcgugan) to say thanks. diff --git a/pull_request_template.md b/.github/pull_request_template.md similarity index 100% rename from pull_request_template.md rename to .github/pull_request_template.md diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index d74c9353d9..93b44bc377 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -13,7 +13,7 @@ jobs: run: shell: bash steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: @@ -40,7 +40,7 @@ jobs: source $VENV pytest tests -v --cov=./rich --cov-report=xml:./coverage.xml --cov-report term-missing - name: Upload code coverage - uses: codecov/codecov-action@v1.0.10 + uses: codecov/codecov-action@v2 with: token: ${{ secrets.CODECOV_TOKEN }} file: ./coverage.xml diff --git a/.gitignore b/.gitignore index bc0dfdb715..a133d7b14f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .pytype .DS_Store .vscode +.idea/ mypy_report docs/build docs/source/_build diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 892dc79187..76f916b96d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,13 +21,12 @@ repos: rev: v1.9.0 hooks: - id: python-check-mock-methods - - id: python-no-eval - id: python-no-log-warn - id: python-use-type-annotations - id: rst-directive-colons - id: rst-inline-touching-normal - repo: https://github.com/psf/black - rev: 21.12b0 + rev: 22.1.0 hooks: - id: black - repo: https://github.com/PyCQA/isort diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b49b5c57c..263e2c851a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,59 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [12.0.0] - 2022-03-10 + +### Added + +- Added options to TimeRemainingColumn to render a compact time format and render elapsed time when a task is + finished. https://github.com/Textualize/rich/pull/1992 +- Added ProgressColumn `MofNCompleteColumn` to display raw `completed/total` column (similar to DownloadColumn, + but displays values as ints, does not convert to floats or add bit/bytes units). + https://github.com/Textualize/rich/pull/1941 +- Replace Colorama with win32 renderer https://github.com/Textualize/rich/pull/1993 +- Add support for namedtuples to `Pretty` https://github.com/Textualize/rich/pull/2031 + +### Fixed + +- In Jupyter mode make the link target be set to "\_blank" +- Fix some issues with markup handling around "[" characters https://github.com/Textualize/rich/pull/1950 +- Fix syntax lexer guessing. +- Fixed Pretty measure not respecting expand_all https://github.com/Textualize/rich/issues/1998 +- Collapsed definitions for single-character spinners, to save memory and reduce import time. +- Fix print_json indent type in `__init__.py` +- Fix error when inspecting object defined in REPL https://github.com/Textualize/rich/pull/2037 +- Fix incorrect highlighting of non-indented JSON https://github.com/Textualize/rich/pull/2038 +- Fixed height reset in complex renderables https://github.com/Textualize/rich/issues/2042 + +### Changed + +- Improved support for enum.Flag in ReprHighlighter https://github.com/Textualize/rich/pull/1920 +- Tree now respects justify=None, i.e. won't pad to right https://github.com/Textualize/rich/issues/1690 +- Removed rich.tabulate which was marked for deprecation +- Deprecated rich.align.AlignValues in favor of AlignMethod + +## [11.2.0] - 2022-02-08 + +### Added + +- Add support for US spelling of "gray" in ANSI color names https://github.com/Textualize/rich/issues/1890 +- Added `rich.diagnose.report` to expose environment debugging logic as function https://github.com/Textualize/rich/pull/1917 +- Added classmethod `Progress.get_default_columns()` to get the default list of progress bar columns https://github.com/Textualize/rich/pull/1894 + +### Fixed + +- Fixed performance issue in measuring text + +### Fixed + +- Fixed test failures on PyPy3 https://github.com/Textualize/rich/pull/1904 + ## [11.1.0] - 2022-01-28 ### Added +- Workaround for edge case of object from Faiss with no `__class__` https://github.com/Textualize/rich/issues/1838 +- Add Traditional Chinese readme - Add `Syntax.guess_lexer`, add support for more lexers (e.g. Django templates etc.) https://github.com/Textualize/rich/pull/1869 - Add `lexer` parameter to `Syntax.from_path` to allow for overrides https://github.com/Textualize/rich/pull/1873 @@ -87,8 +136,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed issue with progress bar not rendering markup https://github.com/willmcgugan/rich/issues/1721 - Fixed race condition when exiting Live https://github.com/willmcgugan/rich/issues/1530 -[10.15.0]: https://github.com/willmcgugan/rich/compare/v10.14.0...v10.15.0 - ## [10.14.0] - 2021-11-16 ### Fixed @@ -385,7 +432,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [9.12.1] - 2021-02-27 -### Fixed +### Fixed - Fixed deadlock in Progress https://github.com/willmcgugan/rich/issues/1061 @@ -442,7 +489,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed error message for tracebacks with broken `__str__` https://github.com/willmcgugan/rich/issues/980 - Fixed markup edge case https://github.com/willmcgugan/rich/issues/987 -### Added +### Added - Added cheeky sponsorship request to test card - Added `quiet` argument to Console constructor @@ -501,7 +548,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [9.8.0] - 2021-01-11 -### Added +### Added - Added **rich_measure** for tree - Added rich.align.VerticalCenter @@ -590,7 +637,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [9.4.0] - 2020-12-12 -### Added +### Added - Added rich.live https://github.com/willmcgugan/rich/pull/382 - Added algin parameter to Rule and Console.rule @@ -599,7 +646,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added style parameter to Console.log - Added rich.diagnose command -### Changed +### Changed - Table.add_row style argument now applies to entire line and not just cells - Added end_section parameter to Table.add_row to force a line underneath row @@ -610,7 +657,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [9.3.0] - 2020-12-1 -### Added +### Added - Added get_datetime parameter to Console, to allow for repeatable tests - Added get_time parameter to Console @@ -710,7 +757,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [8.0.0] - 2020-10-03 -### Added +### Added - Added Console.bell method - Added Set to types that Console.print will automatically pretty print @@ -735,7 +782,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added Console.begin_capture, Console.end_capture and Console.capture - Added Table.title_justify and Table.caption_justify https://github.com/willmcgugan/rich/issues/301 -### Changed +### Changed - Improved formatting of exceptions - Enabled Rich exceptions in logging https://github.com/taliraj @@ -754,7 +801,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added Text.remove_suffix - Added Text.append_tokens -### Changed +### Changed - Text.tabs_to_spaces was renamed to Text.expand_tabs, which works in place rather than returning a new instance - Renamed Column.index to Column.\_index @@ -1038,7 +1085,7 @@ Major version bump for a breaking change to `Text.stylize signature`, which corr ## [3.0.2] - 2020-07-02 -### Added +### Added - Added rich.styled.Styled class to apply styles to renderable - Table.add_row now has an optional style parameter @@ -1151,7 +1198,7 @@ Major version bump for a breaking change to `Text.stylize signature`, which corr - Added 'transient' option to Progress -### Changed +### Changed - Truncated overly long text in Rule with ellipsis overflow @@ -1273,7 +1320,7 @@ Major version bump for a breaking change to `Text.stylize signature`, which corr - Optimizations for Segment, Console and Table -### Added +### Added - Added Console.clear method - Added exporting of links to HTML @@ -1295,7 +1342,7 @@ Major version bump for a breaking change to `Text.stylize signature`, which corr ## [1.1.6] - 2020-05-17 -### Added +### Added - Added rich.align.Align class - Added justify argument to Console.print and console.log @@ -1314,7 +1361,7 @@ Major version bump for a breaking change to `Text.stylize signature`, which corr - Fixes for legacy windows: Bar, Panel, and Rule now use ASCII characters - show_cursor is now a no-op on legacy windows -### Added +### Added - Added Console.input @@ -1616,141 +1663,142 @@ Major version bump for a breaking change to `Text.stylize signature`, which corr - First official release, API still to be stabilized +[unreleased]: https://github.com/willmcgugan/rich/compare/v11.0.0...HEAD +[11.2.0]: https://github.com/willmcgugan/rich/compare/v11.1.0...v11.2.0 [11.1.0]: https://github.com/willmcgugan/rich/compare/v11.0.0...v11.1.0 [11.0.0]: https://github.com/willmcgugan/rich/compare/v10.16.1...v11.0.0 +[10.16.2]: https://github.com/willmcgugan/rich/compare/v10.16.1...v10.16.2 [10.16.1]: https://github.com/willmcgugan/rich/compare/v10.16.0...v10.16.1 [10.16.0]: https://github.com/willmcgugan/rich/compare/v10.15.2...v10.16.0 [10.15.2]: https://github.com/willmcgugan/rich/compare/v10.15.1...v10.15.2 [10.15.1]: https://github.com/willmcgugan/rich/compare/v10.15.0...v10.15.1 -[10.14.0]: https://github.com/willmcgugan/rich/compare/v10.14.0...v10.15.0 -[10.13.0]: https://github.com/willmcgugan/rich/compare/v10.13.0...v10.14.0 -[10.12.0]: https://github.com/willmcgugan/rich/compare/v10.12.0...v10.13.0 -[10.11.0]: https://github.com/willmcgugan/rich/compare/v10.11.0...v10.12.0 -[10.10.0]: https://github.com/willmcgugan/rich/compare/v10.10.0...v10.11.0 -[10.9.0]: https://github.com/willmcgugan/rich/compare/v10.9.0...v10.10.0 -[10.8.0]: https://github.com/willmcgugan/rich/compare/v10.8.0...v10.9.0 -[10.7.0]: https://github.com/willmcgugan/rich/compare/v10.7.0...v10.8.0 -[10.6.0]: https://github.com/willmcgugan/rich/compare/v10.6.0...v10.7.0 -[10.5.0]: https://github.com/willmcgugan/rich/compare/v10.5.0...v10.6.0 -[10.4.0]: https://github.com/willmcgugan/rich/compare/v10.4.0...v10.5.0 -[10.3.0]: https://github.com/willmcgugan/rich/compare/v10.3.0...v10.4.0 -[10.2.2]: https://github.com/willmcgugan/rich/compare/v10.2.2...v10.3.0 -[10.2.1]: https://github.com/willmcgugan/rich/compare/v10.2.1...v10.2.2 -[10.2.0]: https://github.com/willmcgugan/rich/compare/v10.2.0...v10.2.1 -[10.1.0]: https://github.com/willmcgugan/rich/compare/v10.1.0...v10.2.0 -[10.0.1]: https://github.com/willmcgugan/rich/compare/v10.0.1...v10.1.0 -[10.0.0]: https://github.com/willmcgugan/rich/compare/v10.0.0...v10.0.1 -[9.13.0]: https://github.com/willmcgugan/rich/compare/v9.13.0...v10.0.0 -[9.12.4]: https://github.com/willmcgugan/rich/compare/v9.12.4...v9.13.0 -[9.12.3]: https://github.com/willmcgugan/rich/compare/v9.12.3...v9.12.4 -[9.12.2]: https://github.com/willmcgugan/rich/compare/v9.12.2...v9.12.3 -[9.12.1]: https://github.com/willmcgugan/rich/compare/v9.12.1...v9.12.2 -[9.12.0]: https://github.com/willmcgugan/rich/compare/v9.12.0...v9.12.1 -[9.11.1]: https://github.com/willmcgugan/rich/compare/v9.11.1...v9.12.0 -[9.11.0]: https://github.com/willmcgugan/rich/compare/v9.11.0...v9.11.1 -[9.10.0]: https://github.com/willmcgugan/rich/compare/v9.10.0...v9.11.0 -[9.9.0]: https://github.com/willmcgugan/rich/compare/v9.9.0...v9.10.0 -[9.8.2]: https://github.com/willmcgugan/rich/compare/v9.8.2...v9.9.0 -[9.8.1]: https://github.com/willmcgugan/rich/compare/v9.8.1...v9.8.2 -[9.8.0]: https://github.com/willmcgugan/rich/compare/v9.8.0...v9.8.1 -[9.7.0]: https://github.com/willmcgugan/rich/compare/v9.7.0...v9.8.0 -[9.6.2]: https://github.com/willmcgugan/rich/compare/v9.6.2...v9.7.0 -[9.6.1]: https://github.com/willmcgugan/rich/compare/v9.6.1...v9.6.2 -[9.6.0]: https://github.com/willmcgugan/rich/compare/v9.6.0...v9.6.1 -[9.5.1]: https://github.com/willmcgugan/rich/compare/v9.5.1...v9.6.0 -[9.5.0]: https://github.com/willmcgugan/rich/compare/v9.5.0...v9.5.1 -[9.4.0]: https://github.com/willmcgugan/rich/compare/v9.4.0...v9.5.0 -[9.3.0]: https://github.com/willmcgugan/rich/compare/v9.3.0...v9.4.0 -[9.2.0]: https://github.com/willmcgugan/rich/compare/v9.2.0...v9.3.0 -[9.1.0]: https://github.com/willmcgugan/rich/compare/v9.1.0...v9.2.0 -[9.0.1]: https://github.com/willmcgugan/rich/compare/v9.0.1...v9.1.0 -[9.0.0]: https://github.com/willmcgugan/rich/compare/v9.0.0...v9.0.1 -[8.0.0]: https://github.com/willmcgugan/rich/compare/v8.0.0...v9.0.0 -[7.1.0]: https://github.com/willmcgugan/rich/compare/v7.1.0...v8.0.0 -[7.0.0]: https://github.com/willmcgugan/rich/compare/v7.0.0...v7.1.0 -[6.2.0]: https://github.com/willmcgugan/rich/compare/v6.2.0...v7.0.0 -[6.1.2]: https://github.com/willmcgugan/rich/compare/v6.1.2...v6.2.0 -[6.1.1]: https://github.com/willmcgugan/rich/compare/v6.1.1...v6.1.2 -[6.1.0]: https://github.com/willmcgugan/rich/compare/v6.1.0...v6.1.1 -[6.0.0]: https://github.com/willmcgugan/rich/compare/v6.0.0...v6.1.0 -[5.2.1]: https://github.com/willmcgugan/rich/compare/v5.2.1...v6.0.0 -[5.2.0]: https://github.com/willmcgugan/rich/compare/v5.2.0...v5.2.1 -[5.1.2]: https://github.com/willmcgugan/rich/compare/v5.1.2...v5.2.0 -[5.1.1]: https://github.com/willmcgugan/rich/compare/v5.1.1...v5.1.2 -[5.1.0]: https://github.com/willmcgugan/rich/compare/v5.1.0...v5.1.1 -[5.0.0]: https://github.com/willmcgugan/rich/compare/v5.0.0...v5.1.0 -[4.2.2]: https://github.com/willmcgugan/rich/compare/v4.2.2...v5.0.0 -[4.2.1]: https://github.com/willmcgugan/rich/compare/v4.2.1...v4.2.2 -[4.2.0]: https://github.com/willmcgugan/rich/compare/v4.2.0...v4.2.1 -[4.1.0]: https://github.com/willmcgugan/rich/compare/v4.1.0...v4.2.0 -[4.0.0]: https://github.com/willmcgugan/rich/compare/v4.0.0...v4.1.0 -[3.4.1]: https://github.com/willmcgugan/rich/compare/v3.4.1...v4.0.0 -[3.4.0]: https://github.com/willmcgugan/rich/compare/v3.4.0...v3.4.1 -[3.3.2]: https://github.com/willmcgugan/rich/compare/v3.3.2...v3.4.0 -[3.3.1]: https://github.com/willmcgugan/rich/compare/v3.3.1...v3.3.2 -[3.3.0]: https://github.com/willmcgugan/rich/compare/v3.3.0...v3.3.1 -[3.2.0]: https://github.com/willmcgugan/rich/compare/v3.2.0...v3.3.0 -[3.1.0]: https://github.com/willmcgugan/rich/compare/v3.1.0...v3.2.0 -[3.0.5]: https://github.com/willmcgugan/rich/compare/v3.0.5...v3.1.0 -[3.0.4]: https://github.com/willmcgugan/rich/compare/v3.0.4...v3.0.5 -[3.0.3]: https://github.com/willmcgugan/rich/compare/v3.0.3...v3.0.4 -[3.0.2]: https://github.com/willmcgugan/rich/compare/v3.0.2...v3.0.3 -[3.0.1]: https://github.com/willmcgugan/rich/compare/v3.0.1...v3.0.2 -[3.0.0]: https://github.com/willmcgugan/rich/compare/v3.0.0...v3.0.1 -[2.3.1]: https://github.com/willmcgugan/rich/compare/v2.3.1...v3.0.0 -[2.3.0]: https://github.com/willmcgugan/rich/compare/v2.3.0...v2.3.1 -[2.2.6]: https://github.com/willmcgugan/rich/compare/v2.2.6...v2.3.0 -[2.2.5]: https://github.com/willmcgugan/rich/compare/v2.2.5...v2.2.6 -[2.2.4]: https://github.com/willmcgugan/rich/compare/v2.2.4...v2.2.5 -[2.2.3]: https://github.com/willmcgugan/rich/compare/v2.2.3...v2.2.4 -[2.2.2]: https://github.com/willmcgugan/rich/compare/v2.2.2...v2.2.3 -[2.2.1]: https://github.com/willmcgugan/rich/compare/v2.2.1...v2.2.2 -[2.2.0]: https://github.com/willmcgugan/rich/compare/v2.2.0...v2.2.1 -[2.1.0]: https://github.com/willmcgugan/rich/compare/v2.1.0...v2.2.0 -[2.0.1]: https://github.com/willmcgugan/rich/compare/v2.0.1...v2.1.0 -[2.0.0]: https://github.com/willmcgugan/rich/compare/v2.0.0...v2.0.1 -[1.3.1]: https://github.com/willmcgugan/rich/compare/v1.3.1...v2.0.0 -[1.3.0]: https://github.com/willmcgugan/rich/compare/v1.3.0...v1.3.1 -[1.2.3]: https://github.com/willmcgugan/rich/compare/v1.2.3...v1.3.0 -[1.2.2]: https://github.com/willmcgugan/rich/compare/v1.2.2...v1.2.3 -[1.2.1]: https://github.com/willmcgugan/rich/compare/v1.2.1...v1.2.2 -[1.2.0]: https://github.com/willmcgugan/rich/compare/v1.2.0...v1.2.1 -[1.1.9]: https://github.com/willmcgugan/rich/compare/v1.1.9...v1.2.0 -[1.1.8]: https://github.com/willmcgugan/rich/compare/v1.1.8...v1.1.9 -[1.1.7]: https://github.com/willmcgugan/rich/compare/v1.1.7...v1.1.8 -[1.1.6]: https://github.com/willmcgugan/rich/compare/v1.1.6...v1.1.7 -[1.1.5]: https://github.com/willmcgugan/rich/compare/v1.1.5...v1.1.6 -[1.1.4]: https://github.com/willmcgugan/rich/compare/v1.1.4...v1.1.5 -[1.1.3]: https://github.com/willmcgugan/rich/compare/v1.1.3...v1.1.4 -[1.1.2]: https://github.com/willmcgugan/rich/compare/v1.1.2...v1.1.3 -[1.1.1]: https://github.com/willmcgugan/rich/compare/v1.1.1...v1.1.2 -[1.1.0]: https://github.com/willmcgugan/rich/compare/v1.1.0...v1.1.1 -[1.0.3]: https://github.com/willmcgugan/rich/compare/v1.0.3...v1.1.0 -[1.0.2]: https://github.com/willmcgugan/rich/compare/v1.0.2...v1.0.3 -[1.0.1]: https://github.com/willmcgugan/rich/compare/v1.0.1...v1.0.2 -[1.0.0]: https://github.com/willmcgugan/rich/compare/v1.0.0...v1.0.1 -[0.8.13]: https://github.com/willmcgugan/rich/compare/v0.8.13...v1.0.0 -[0.8.12]: https://github.com/willmcgugan/rich/compare/v0.8.12...v0.8.13 -[0.8.11]: https://github.com/willmcgugan/rich/compare/v0.8.11...v0.8.12 -[0.8.10]: https://github.com/willmcgugan/rich/compare/v0.8.10...v0.8.11 -[0.8.9]: https://github.com/willmcgugan/rich/compare/v0.8.9...v0.8.10 -[0.8.8]: https://github.com/willmcgugan/rich/compare/v0.8.8...v0.8.9 -[0.8.7]: https://github.com/willmcgugan/rich/compare/v0.8.7...v0.8.8 -[0.8.6]: https://github.com/willmcgugan/rich/compare/v0.8.6...v0.8.7 -[0.8.5]: https://github.com/willmcgugan/rich/compare/v0.8.5...v0.8.6 -[0.8.4]: https://github.com/willmcgugan/rich/compare/v0.8.4...v0.8.5 -[0.8.3]: https://github.com/willmcgugan/rich/compare/v0.8.3...v0.8.4 -[0.8.2]: https://github.com/willmcgugan/rich/compare/v0.8.2...v0.8.3 -[0.8.1]: https://github.com/willmcgugan/rich/compare/v0.8.1...v0.8.2 -[0.8.0]: https://github.com/willmcgugan/rich/compare/v0.8.0...v0.8.1 -[0.7.2]: https://github.com/willmcgugan/rich/compare/v0.7.2...v0.8.0 -[0.7.1]: https://github.com/willmcgugan/rich/compare/v0.7.1...v0.7.2 -[0.7.0]: https://github.com/willmcgugan/rich/compare/v0.7.0...v0.7.1 -[0.6.0]: https://github.com/willmcgugan/rich/compare/v0.6.0...v0.7.0 -[0.5.0]: https://github.com/willmcgugan/rich/compare/v0.5.0...v0.6.0 -[0.4.1]: https://github.com/willmcgugan/rich/compare/v0.4.1...v0.5.0 -[0.4.0]: https://github.com/willmcgugan/rich/compare/v0.4.0...v0.4.1 -[0.3.3]: https://github.com/willmcgugan/rich/compare/v0.3.3...v0.4.0 -[0.3.2]: https://github.com/willmcgugan/rich/compare/v0.3.2...v0.3.3 -[0.3.1]: https://github.com/willmcgugan/rich/compare/v0.3.1...v0.3.2 -[0.3.0]: https://github.com/willmcgugan/rich/compare/v0.3.0...v0.3.1 +[10.15.0]: https://github.com/willmcgugan/rich/compare/v10.14.0...v10.15.0 +[10.14.0]: https://github.com/willmcgugan/rich/compare/v10.13.0...v10.14.0 +[10.13.0]: https://github.com/willmcgugan/rich/compare/v10.12.0...v10.13.0 +[10.12.0]: https://github.com/willmcgugan/rich/compare/v10.11.0...v10.12.0 +[10.11.0]: https://github.com/willmcgugan/rich/compare/v10.10.0...v10.11.0 +[10.10.0]: https://github.com/willmcgugan/rich/compare/v10.9.0...v10.10.0 +[10.9.0]: https://github.com/willmcgugan/rich/compare/v10.8.0...v10.9.0 +[10.8.0]: https://github.com/willmcgugan/rich/compare/v10.7.0...v10.8.0 +[10.7.0]: https://github.com/willmcgugan/rich/compare/v10.6.0...v10.7.0 +[10.6.0]: https://github.com/willmcgugan/rich/compare/v10.5.0...v10.6.0 +[10.5.0]: https://github.com/willmcgugan/rich/compare/v10.4.0...v10.5.0 +[10.4.0]: https://github.com/willmcgugan/rich/compare/v10.3.0...v10.4.0 +[10.3.0]: https://github.com/willmcgugan/rich/compare/v10.2.2...v10.3.0 +[10.2.2]: https://github.com/willmcgugan/rich/compare/v10.2.1...v10.2.2 +[10.2.1]: https://github.com/willmcgugan/rich/compare/v10.2.0...v10.2.1 +[10.2.0]: https://github.com/willmcgugan/rich/compare/v10.1.0...v10.2.0 +[10.1.0]: https://github.com/willmcgugan/rich/compare/v10.0.1...v10.1.0 +[10.0.1]: https://github.com/willmcgugan/rich/compare/v10.0.0...v10.0.1 +[10.0.0]: https://github.com/willmcgugan/rich/compare/v9.13.0...v10.0.0 +[9.13.0]: https://github.com/willmcgugan/rich/compare/v9.12.4...v9.13.0 +[9.12.4]: https://github.com/willmcgugan/rich/compare/v9.12.3...v9.12.4 +[9.12.3]: https://github.com/willmcgugan/rich/compare/v9.12.2...v9.12.3 +[9.12.2]: https://github.com/willmcgugan/rich/compare/v9.12.1...v9.12.2 +[9.12.1]: https://github.com/willmcgugan/rich/compare/v9.12.0...v9.12.1 +[9.12.0]: https://github.com/willmcgugan/rich/compare/v9.11.1...v9.12.0 +[9.11.1]: https://github.com/willmcgugan/rich/compare/v9.11.0...v9.11.1 +[9.11.0]: https://github.com/willmcgugan/rich/compare/v9.10.0...v9.11.0 +[9.10.0]: https://github.com/willmcgugan/rich/compare/v9.9.0...v9.10.0 +[9.9.0]: https://github.com/willmcgugan/rich/compare/v9.8.2...v9.9.0 +[9.8.2]: https://github.com/willmcgugan/rich/compare/v9.8.1...v9.8.2 +[9.8.1]: https://github.com/willmcgugan/rich/compare/v9.8.0...v9.8.1 +[9.8.0]: https://github.com/willmcgugan/rich/compare/v9.7.0...v9.8.0 +[9.7.0]: https://github.com/willmcgugan/rich/compare/v9.6.2...v9.7.0 +[9.6.2]: https://github.com/willmcgugan/rich/compare/v9.6.1...v9.6.2 +[9.6.1]: https://github.com/willmcgugan/rich/compare/v9.6.0...v9.6.1 +[9.6.0]: https://github.com/willmcgugan/rich/compare/v9.5.1...v9.6.0 +[9.5.1]: https://github.com/willmcgugan/rich/compare/v9.5.0...v9.5.1 +[9.5.0]: https://github.com/willmcgugan/rich/compare/v9.4.0...v9.5.0 +[9.4.0]: https://github.com/willmcgugan/rich/compare/v9.3.0...v9.4.0 +[9.3.0]: https://github.com/willmcgugan/rich/compare/v9.2.0...v9.3.0 +[9.2.0]: https://github.com/willmcgugan/rich/compare/v9.1.0...v9.2.0 +[9.1.0]: https://github.com/willmcgugan/rich/compare/v9.0.1...v9.1.0 +[9.0.1]: https://github.com/willmcgugan/rich/compare/v9.0.0...v9.0.1 +[9.0.0]: https://github.com/willmcgugan/rich/compare/v8.0.0...v9.0.0 +[8.0.0]: https://github.com/willmcgugan/rich/compare/v7.1.0...v8.0.0 +[7.1.0]: https://github.com/willmcgugan/rich/compare/v7.0.0...v7.1.0 +[7.0.0]: https://github.com/willmcgugan/rich/compare/v6.2.0...v7.0.0 +[6.2.0]: https://github.com/willmcgugan/rich/compare/v6.1.2...v6.2.0 +[6.1.2]: https://github.com/willmcgugan/rich/compare/v6.1.1...v6.1.2 +[6.1.1]: https://github.com/willmcgugan/rich/compare/v6.1.0...v6.1.1 +[6.1.0]: https://github.com/willmcgugan/rich/compare/v6.0.0...v6.1.0 +[6.0.0]: https://github.com/willmcgugan/rich/compare/v5.2.1...v6.0.0 +[5.2.1]: https://github.com/willmcgugan/rich/compare/v5.2.0...v5.2.1 +[5.2.0]: https://github.com/willmcgugan/rich/compare/v5.1.2...v5.2.0 +[5.1.2]: https://github.com/willmcgugan/rich/compare/v5.1.1...v5.1.2 +[5.1.1]: https://github.com/willmcgugan/rich/compare/v5.1.0...v5.1.1 +[5.1.0]: https://github.com/willmcgugan/rich/compare/v5.0.0...v5.1.0 +[5.0.0]: https://github.com/willmcgugan/rich/compare/v4.2.2...v5.0.0 +[4.2.2]: https://github.com/willmcgugan/rich/compare/v4.2.1...v4.2.2 +[4.2.1]: https://github.com/willmcgugan/rich/compare/v4.2.0...v4.2.1 +[4.2.0]: https://github.com/willmcgugan/rich/compare/v4.1.0...v4.2.0 +[4.1.0]: https://github.com/willmcgugan/rich/compare/v4.0.0...v4.1.0 +[4.0.0]: https://github.com/willmcgugan/rich/compare/v3.4.1...v4.0.0 +[3.4.1]: https://github.com/willmcgugan/rich/compare/v3.4.0...v3.4.1 +[3.4.0]: https://github.com/willmcgugan/rich/compare/v3.3.2...v3.4.0 +[3.3.2]: https://github.com/willmcgugan/rich/compare/v3.3.1...v3.3.2 +[3.3.1]: https://github.com/willmcgugan/rich/compare/v3.3.0...v3.3.1 +[3.3.0]: https://github.com/willmcgugan/rich/compare/v3.2.0...v3.3.0 +[3.2.0]: https://github.com/willmcgugan/rich/compare/v3.1.0...v3.2.0 +[3.1.0]: https://github.com/willmcgugan/rich/compare/v3.0.5...v3.1.0 +[3.0.5]: https://github.com/willmcgugan/rich/compare/v3.0.4...v3.0.5 +[3.0.4]: https://github.com/willmcgugan/rich/compare/v3.0.3...v3.0.4 +[3.0.3]: https://github.com/willmcgugan/rich/compare/v3.0.2...v3.0.3 +[3.0.2]: https://github.com/willmcgugan/rich/compare/v3.0.1...v3.0.2 +[3.0.1]: https://github.com/willmcgugan/rich/compare/v3.0.0...v3.0.1 +[3.0.0]: https://github.com/willmcgugan/rich/compare/v2.3.1...v3.0.0 +[2.3.1]: https://github.com/willmcgugan/rich/compare/v2.3.0...v2.3.1 +[2.3.0]: https://github.com/willmcgugan/rich/compare/v2.2.6...v2.3.0 +[2.2.6]: https://github.com/willmcgugan/rich/compare/v2.2.5...v2.2.6 +[2.2.5]: https://github.com/willmcgugan/rich/compare/v2.2.4...v2.2.5 +[2.2.4]: https://github.com/willmcgugan/rich/compare/v2.2.3...v2.2.4 +[2.2.3]: https://github.com/willmcgugan/rich/compare/v2.2.2...v2.2.3 +[2.2.2]: https://github.com/willmcgugan/rich/compare/v2.2.1...v2.2.2 +[2.2.1]: https://github.com/willmcgugan/rich/compare/v2.2.0...v2.2.1 +[2.2.0]: https://github.com/willmcgugan/rich/compare/v2.1.0...v2.2.0 +[2.1.0]: https://github.com/willmcgugan/rich/compare/v2.0.1...v2.1.0 +[2.0.1]: https://github.com/willmcgugan/rich/compare/v2.0.0...v2.0.1 +[2.0.0]: https://github.com/willmcgugan/rich/compare/v1.3.1...v2.0.0 +[1.3.1]: https://github.com/willmcgugan/rich/compare/v1.3.0...v1.3.1 +[1.3.0]: https://github.com/willmcgugan/rich/compare/v1.2.3...v1.3.0 +[1.2.3]: https://github.com/willmcgugan/rich/compare/v1.2.2...v1.2.3 +[1.2.2]: https://github.com/willmcgugan/rich/compare/v1.2.1...v1.2.2 +[1.2.1]: https://github.com/willmcgugan/rich/compare/v1.2.0...v1.2.1 +[1.2.0]: https://github.com/willmcgugan/rich/compare/v1.1.9...v1.2.0 +[1.1.9]: https://github.com/willmcgugan/rich/compare/v1.1.8...v1.1.9 +[1.1.8]: https://github.com/willmcgugan/rich/compare/v1.1.7...v1.1.8 +[1.1.7]: https://github.com/willmcgugan/rich/compare/v1.1.6...v1.1.7 +[1.1.6]: https://github.com/willmcgugan/rich/compare/v1.1.5...v1.1.6 +[1.1.5]: https://github.com/willmcgugan/rich/compare/v1.1.4...v1.1.5 +[1.1.4]: https://github.com/willmcgugan/rich/compare/v1.1.3...v1.1.4 +[1.1.3]: https://github.com/willmcgugan/rich/compare/v1.1.2...v1.1.3 +[1.1.2]: https://github.com/willmcgugan/rich/compare/v1.1.1...v1.1.2 +[1.1.1]: https://github.com/willmcgugan/rich/compare/v1.1.0...v1.1.1 +[1.1.0]: https://github.com/willmcgugan/rich/compare/v1.0.3...v1.1.0 +[1.0.3]: https://github.com/willmcgugan/rich/compare/v1.0.2...v1.0.3 +[1.0.2]: https://github.com/willmcgugan/rich/compare/v1.0.1...v1.0.2 +[1.0.1]: https://github.com/willmcgugan/rich/compare/v1.0.0...v1.0.1 +[1.0.0]: https://github.com/willmcgugan/rich/compare/v0.8.13...v1.0.0 +[0.8.13]: https://github.com/willmcgugan/rich/compare/v0.8.12...v0.8.13 +[0.8.12]: https://github.com/willmcgugan/rich/compare/v0.8.11...v0.8.12 +[0.8.11]: https://github.com/willmcgugan/rich/compare/v0.8.10...v0.8.11 +[0.8.10]: https://github.com/willmcgugan/rich/compare/v0.8.9...v0.8.10 +[0.8.9]: https://github.com/willmcgugan/rich/compare/v0.8.8...v0.8.9 +[0.8.8]: https://github.com/willmcgugan/rich/compare/v0.8.7...v0.8.8 +[0.8.7]: https://github.com/willmcgugan/rich/compare/v0.8.6...v0.8.7 +[0.8.6]: https://github.com/willmcgugan/rich/compare/v0.8.5...v0.8.6 +[0.8.5]: https://github.com/willmcgugan/rich/compare/v0.8.4...v0.8.5 +[0.8.4]: https://github.com/willmcgugan/rich/compare/v0.8.3...v0.8.4 +[0.8.3]: https://github.com/willmcgugan/rich/compare/v0.8.2...v0.8.3 +[0.8.2]: https://github.com/willmcgugan/rich/compare/v0.8.1...v0.8.2 +[0.8.1]: https://github.com/willmcgugan/rich/compare/v0.8.0...v0.8.1 +[0.8.0]: https://github.com/willmcgugan/rich/compare/v0.7.2...v0.8.0 +[0.7.2]: https://github.com/willmcgugan/rich/compare/v0.7.1...v0.7.2 +[0.7.1]: https://github.com/willmcgugan/rich/compare/v0.7.0...v0.7.1 +[0.7.0]: https://github.com/willmcgugan/rich/compare/v0.6.0...v0.7.0 +[0.6.0]: https://github.com/willmcgugan/rich/compare/v0.5.0...v0.6.0 +[0.5.0]: https://github.com/willmcgugan/rich/compare/v0.4.1...v0.5.0 +[0.4.1]: https://github.com/willmcgugan/rich/compare/v0.4.0...v0.4.1 +[0.4.0]: https://github.com/willmcgugan/rich/compare/v0.3.3...v0.4.0 +[0.3.3]: https://github.com/willmcgugan/rich/compare/v0.3.2...v0.3.3 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index c29cd79049..140f77f431 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -4,11 +4,16 @@ The following people have contributed to the development of Rich: +- [Patrick Arminio](https://github.com/patrick91) - [Gregory Beauregard](https://github.com/GBeauregard/pyffstream) +- [Dennis Brakhane](https://github.com/brakhane) - [Darren Burns](https://github.com/darrenburns) +- [Ed Davis](https://github.com/davised) - [Pete Davison](https://github.com/pd93) - [James Estevez](https://github.com/jstvz) - [Oleksis Fraga](https://github.com/oleksis) +- [Michał Górny](https://github.com/mgorny) +- [Leron Gray](https://github.com/daddycocoaman) - [Kenneth Hoste](https://github.com/boegel) - [Finn Hughes](https://github.com/finnhughes) - [Josh Karpel](https://github.com/JoshKarpel) @@ -16,14 +21,18 @@ The following people have contributed to the development of Rich: - [Hedy Li](https://github.com/hedythedev) - [Alexander Mancevice](https://github.com/amancevice) - [Will McGugan](https://github.com/willmcgugan) +- [Paul McGuire](https://github.com/ptmcg) - [Nathan Page](https://github.com/nathanrpage97) - [Avi Perl](https://github.com/avi-perl) - [Laurent Peuch](https://github.com/psycojoker) - [Kylian Point](https://github.com/p0lux) - [Kyle Pollina](https://github.com/kylepollina) - [Clément Robert](https://github.com/neutrinoceros) +- [Brian Rutledge](https://github.com/bhrutledge) - [Tushar Sadhwani](https://github.com/tusharsadhwani) +- [Paul Sanders](https://github.com/sanders41) - [Tim Savage](https://github.com/timsavage) - [Nicolas Simonds](https://github.com/0xDEC0DE) +- [Aaron Stephens](https://github.com/aaronst) - [Gabriele N. Tornetta](https://github.com/p403n1x87) -- [Patrick Arminio](https://github.com/patrick91) +- [Arian Mollik Wasi](https://github.com/wasi-master) diff --git a/README.cn.md b/README.cn.md index 3fc571cd72..a5c7c5b66a 100644 --- a/README.cn.md +++ b/README.cn.md @@ -7,7 +7,8 @@ ![Logo](https://github.com/willmcgugan/rich/raw/master/imgs/logo.svg) [English readme](https://github.com/willmcgugan/rich/blob/master/README.md) - • [中文 readme](https://github.com/willmcgugan/rich/blob/master/README.cn.md) + • [简体中文 readme](https://github.com/willmcgugan/rich/blob/master/README.cn.md) + • [正體中文 readme](https://github.com/willmcgugan/rich/blob/master/README.zh-tw.md) • [Lengua española readme](https://github.com/willmcgugan/rich/blob/master/README.es.md) • [Deutsche readme](https://github.com/willmcgugan/rich/blob/master/README.de.md) • [Läs på svenska](https://github.com/willmcgugan/rich/blob/master/README.sv.md) diff --git a/README.de-ch.md b/README.de-ch.md index e965753221..6c1d7d278b 100644 --- a/README.de-ch.md +++ b/README.de-ch.md @@ -7,7 +7,8 @@ ![Logo](https://github.com/willmcgugan/rich/raw/master/imgs/logo.svg) [English readme](https://github.com/willmcgugan/rich/blob/master/README.md) - • [中文 readme](https://github.com/willmcgugan/rich/blob/master/README.cn.md) + • [简体中文 readme](https://github.com/willmcgugan/rich/blob/master/README.cn.md) + • [正體中文 readme](https://github.com/willmcgugan/rich/blob/master/README.zh-tw.md) • [Lengua española readme](https://github.com/willmcgugan/rich/blob/master/README.es.md) • [Deutsche readme](https://github.com/willmcgugan/rich/blob/master/README.de.md) • [Läs på svenska](https://github.com/willmcgugan/rich/blob/master/README.sv.md) diff --git a/README.de.md b/README.de.md index 38cf845d79..34a99a64e8 100644 --- a/README.de.md +++ b/README.de.md @@ -7,7 +7,8 @@ ![Logo](https://github.com/willmcgugan/rich/raw/master/imgs/logo.svg) [English readme](https://github.com/willmcgugan/rich/blob/master/README.md) - • [中文 readme](https://github.com/willmcgugan/rich/blob/master/README.cn.md) + • [简体中文 readme](https://github.com/willmcgugan/rich/blob/master/README.cn.md) + • [正體中文 readme](https://github.com/willmcgugan/rich/blob/master/README.zh-tw.md) • [Lengua española readme](https://github.com/willmcgugan/rich/blob/master/README.es.md) • [Deutsche readme](https://github.com/willmcgugan/rich/blob/master/README.de.md) • [Läs på svenska](https://github.com/willmcgugan/rich/blob/master/README.sv.md) diff --git a/README.es.md b/README.es.md index a3aa631cab..c9cb2af141 100644 --- a/README.es.md +++ b/README.es.md @@ -7,7 +7,8 @@ ![Logo](https://github.com/willmcgugan/rich/raw/master/imgs/logo.svg) [English readme](https://github.com/willmcgugan/rich/blob/master/README.md) - • [中文 readme](https://github.com/willmcgugan/rich/blob/master/README.cn.md) + • [简体中文 readme](https://github.com/willmcgugan/rich/blob/master/README.cn.md) + • [正體中文 readme](https://github.com/willmcgugan/rich/blob/master/README.zh-tw.md) • [Lengua española readme](https://github.com/willmcgugan/rich/blob/master/README.es.md) • [Deutsche readme](https://github.com/willmcgugan/rich/blob/master/README.de.md) • [Läs på svenska](https://github.com/willmcgugan/rich/blob/master/README.sv.md) diff --git a/README.fr.md b/README.fr.md index 394f03bf83..1a09c054a1 100644 --- a/README.fr.md +++ b/README.fr.md @@ -7,7 +7,8 @@ ![Logo](https://github.com/willmcgugan/rich/raw/master/imgs/logo.svg) [English readme](https://github.com/willmcgugan/rich/blob/master/README.md) - • [中文 readme](https://github.com/willmcgugan/rich/blob/master/README.cn.md) + • [简体中文 readme](https://github.com/willmcgugan/rich/blob/master/README.cn.md) + • [正體中文 readme](https://github.com/willmcgugan/rich/blob/master/README.zh-tw.md) • [Lengua española readme](https://github.com/willmcgugan/rich/blob/master/README.es.md) • [Deutsche readme](https://github.com/willmcgugan/rich/blob/master/README.de.md) • [Läs på svenska](https://github.com/willmcgugan/rich/blob/master/README.sv.md) diff --git a/README.hi.md b/README.hi.md index 9c3009f4fa..c3c62f5321 100644 --- a/README.hi.md +++ b/README.hi.md @@ -7,7 +7,8 @@ ![Logo](https://github.com/willmcgugan/rich/raw/master/imgs/logo.svg) [English readme](https://github.com/willmcgugan/rich/blob/master/README.md) - • [中文 readme](https://github.com/willmcgugan/rich/blob/master/README.cn.md) + • [简体中文 readme](https://github.com/willmcgugan/rich/blob/master/README.cn.md) + • [正體中文 readme](https://github.com/willmcgugan/rich/blob/master/README.zh-tw.md) • [Lengua española readme](https://github.com/willmcgugan/rich/blob/master/README.es.md) • [Deutsche readme](https://github.com/willmcgugan/rich/blob/master/README.de.md) • [Läs på svenska](https://github.com/willmcgugan/rich/blob/master/README.sv.md) diff --git a/README.it.md b/README.it.md index 7d0b68d714..c6e8b5620c 100644 --- a/README.it.md +++ b/README.it.md @@ -6,7 +6,8 @@ ![Logo](https://github.com/willmcgugan/rich/raw/master/imgs/logo.svg) [English readme](https://github.com/willmcgugan/rich/blob/master/README.md) - • [中文 readme](https://github.com/willmcgugan/rich/blob/master/README.cn.md) + • [简体中文 readme](https://github.com/willmcgugan/rich/blob/master/README.cn.md) + • [正體中文 readme](https://github.com/willmcgugan/rich/blob/master/README.zh-tw.md) • [Lengua española readme](https://github.com/willmcgugan/rich/blob/master/README.es.md) • [Deutsche readme](https://github.com/willmcgugan/rich/blob/master/README.de.md) • [Läs på svenska](https://github.com/willmcgugan/rich/blob/master/README.sv.md) diff --git a/README.ja.md b/README.ja.md index 1bec484597..300f779d31 100644 --- a/README.ja.md +++ b/README.ja.md @@ -7,7 +7,8 @@ ![Logo](https://github.com/willmcgugan/rich/raw/master/imgs/logo.svg) [English readme](https://github.com/willmcgugan/rich/blob/master/README.md) - • [中文 readme](https://github.com/willmcgugan/rich/blob/master/README.cn.md) + • [简体中文 readme](https://github.com/willmcgugan/rich/blob/master/README.cn.md) + • [正體中文 readme](https://github.com/willmcgugan/rich/blob/master/README.zh-tw.md) • [Lengua española readme](https://github.com/willmcgugan/rich/blob/master/README.es.md) • [Deutsche readme](https://github.com/willmcgugan/rich/blob/master/README.de.md) • [Läs på svenska](https://github.com/willmcgugan/rich/blob/master/README.sv.md) diff --git a/README.kr.md b/README.kr.md index a1a67ea95d..b55529af72 100644 --- a/README.kr.md +++ b/README.kr.md @@ -7,7 +7,8 @@ ![Logo](https://github.com/willmcgugan/rich/raw/master/imgs/logo.svg) [English readme](https://github.com/willmcgugan/rich/blob/master/README.md) - • [中文 readme](https://github.com/willmcgugan/rich/blob/master/README.cn.md) + • [简体中文 readme](https://github.com/willmcgugan/rich/blob/master/README.cn.md) + • [正體中文 readme](https://github.com/willmcgugan/rich/blob/master/README.zh-tw.md) • [Lengua española readme](https://github.com/willmcgugan/rich/blob/master/README.es.md) • [Deutsche readme](https://github.com/willmcgugan/rich/blob/master/README.de.md) • [Läs på svenska](https://github.com/willmcgugan/rich/blob/master/README.sv.md) diff --git a/README.md b/README.md index f5365e7d0f..97fca8ade5 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,15 @@ [![Supported Python Versions](https://img.shields.io/pypi/pyversions/rich/10.11.0)](https://pypi.org/project/rich/) [![PyPI version](https://badge.fury.io/py/rich.svg)](https://badge.fury.io/py/rich) [![Downloads](https://pepy.tech/badge/rich/month)](https://pepy.tech/project/rich) -[![codecov](https://codecov.io/gh/willmcgugan/rich/branch/master/graph/badge.svg)](https://codecov.io/gh/willmcgugan/rich) +[![codecov](https://img.shields.io/codecov/c/github/Textualize/rich?label=codecov&logo=codecov)](https://codecov.io/gh/willmcgugan/rich) [![Rich blog](https://img.shields.io/badge/blog-rich%20news-yellowgreen)](https://www.willmcgugan.com/tag/rich/) [![Twitter Follow](https://img.shields.io/twitter/follow/willmcgugan.svg?style=social)](https://twitter.com/willmcgugan) ![Logo](https://github.com/willmcgugan/rich/raw/master/imgs/logo.svg) [English readme](https://github.com/willmcgugan/rich/blob/master/README.md) - • [中文 readme](https://github.com/willmcgugan/rich/blob/master/README.cn.md) + • [简体中文 readme](https://github.com/willmcgugan/rich/blob/master/README.cn.md) + • [正體中文 readme](https://github.com/willmcgugan/rich/blob/master/README.zh-tw.md) • [Lengua española readme](https://github.com/willmcgugan/rich/blob/master/README.es.md) • [Deutsche readme](https://github.com/willmcgugan/rich/blob/master/README.de.md) • [Läs på svenska](https://github.com/willmcgugan/rich/blob/master/README.sv.md) @@ -418,11 +419,14 @@ Here's what it looks like on OSX (similar on Linux): All Rich renderables make use of the [Console Protocol](https://rich.readthedocs.io/en/latest/protocol.html), which you can also use to implement your own Rich content. -# Rich for enterprise +# Rich CLI -Available as part of the Tidelift Subscription. -The maintainers of Rich and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source packages you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact packages you use. [Learn more.](https://tidelift.com/subscription/pkg/pypi-rich?utm_source=pypi-rich&utm_medium=referral&utm_campaign=enterprise&utm_term=repo) +See also [Rich CLI](https://github.com/textualize/rich-cli) for a command line application powered by Rich. Syntax highlight code, render markdown, display CSVs in tables, and more, directly from the command prompt. + + +![Rich CLI](https://raw.githubusercontent.com/Textualize/rich-cli/main/imgs/rich-cli-splash.jpg) + # Projects using Rich diff --git a/README.pt-br.md b/README.pt-br.md index c2294502ea..55f366cafd 100644 --- a/README.pt-br.md +++ b/README.pt-br.md @@ -7,7 +7,8 @@ ![Logo](https://github.com/willmcgugan/rich/raw/master/imgs/logo.svg) [English readme](https://github.com/willmcgugan/rich/blob/master/README.md) - • [中文 readme](https://github.com/willmcgugan/rich/blob/master/README.cn.md) + • [简体中文 readme](https://github.com/willmcgugan/rich/blob/master/README.cn.md) + • [正體中文 readme](https://github.com/willmcgugan/rich/blob/master/README.zh-tw.md) • [Lengua española readme](https://github.com/willmcgugan/rich/blob/master/README.es.md) • [Deutsche readme](https://github.com/willmcgugan/rich/blob/master/README.de.md) • [Läs på svenska](https://github.com/willmcgugan/rich/blob/master/README.sv.md) diff --git a/README.ru.md b/README.ru.md index 4223309d97..b6ff2d255e 100644 --- a/README.ru.md +++ b/README.ru.md @@ -8,7 +8,8 @@ ![Logo](https://github.com/willmcgugan/rich/raw/master/imgs/logo.svg) [English readme](https://github.com/willmcgugan/rich/blob/master/README.md) - • [中文 readme](https://github.com/willmcgugan/rich/blob/master/README.cn.md) + • [简体中文 readme](https://github.com/willmcgugan/rich/blob/master/README.cn.md) + • [正體中文 readme](https://github.com/willmcgugan/rich/blob/master/README.zh-tw.md) • [Lengua española readme](https://github.com/willmcgugan/rich/blob/master/README.es.md) • [Deutsche readme](https://github.com/willmcgugan/rich/blob/master/README.de.md) • [Läs på svenska](https://github.com/willmcgugan/rich/blob/master/README.sv.md) diff --git a/README.sv.md b/README.sv.md index 35b2669791..b2685a705e 100644 --- a/README.sv.md +++ b/README.sv.md @@ -7,7 +7,8 @@ ![Logo](https://github.com/willmcgugan/rich/raw/master/imgs/logo.svg) [English readme](https://github.com/willmcgugan/rich/blob/master/README.md) - • [中文 readme](https://github.com/willmcgugan/rich/blob/master/README.cn.md) + • [简体中文 readme](https://github.com/willmcgugan/rich/blob/master/README.cn.md) + • [正體中文 readme](https://github.com/willmcgugan/rich/blob/master/README.zh-tw.md) • [Lengua española readme](https://github.com/willmcgugan/rich/blob/master/README.es.md) • [Deutsche readme](https://github.com/willmcgugan/rich/blob/master/README.de.md) • [Läs på svenska](https://github.com/willmcgugan/rich/blob/master/README.sv.md) diff --git a/README.zh-tw.md b/README.zh-tw.md new file mode 100644 index 0000000000..207f3260f7 --- /dev/null +++ b/README.zh-tw.md @@ -0,0 +1,457 @@ +[![Downloads](https://pepy.tech/badge/rich/month)](https://pepy.tech/project/rich) +[![PyPI version](https://badge.fury.io/py/rich.svg)](https://badge.fury.io/py/rich) +[![codecov](https://codecov.io/gh/willmcgugan/rich/branch/master/graph/badge.svg)](https://codecov.io/gh/willmcgugan/rich) +[![Rich blog](https://img.shields.io/badge/blog-rich%20news-yellowgreen)](https://www.willmcgugan.com/tag/rich/) +[![Twitter Follow](https://img.shields.io/twitter/follow/willmcgugan.svg?style=social)](https://twitter.com/willmcgugan) + +![Logo](https://github.com/willmcgugan/rich/raw/master/imgs/logo.svg) + +[English readme](https://github.com/willmcgugan/rich/blob/master/README.md) + • [简体中文 readme](https://github.com/willmcgugan/rich/blob/master/README.cn.md) + • [正體中文 readme](https://github.com/willmcgugan/rich/blob/master/README.zh-tw.md) + • [Lengua española readme](https://github.com/willmcgugan/rich/blob/master/README.es.md) + • [Deutsche readme](https://github.com/willmcgugan/rich/blob/master/README.de.md) + • [Läs på svenska](https://github.com/willmcgugan/rich/blob/master/README.sv.md) + • [日本語 readme](https://github.com/willmcgugan/rich/blob/master/README.ja.md) + • [한국어 readme](https://github.com/willmcgugan/rich/blob/master/README.kr.md) + • [Français readme](https://github.com/willmcgugan/rich/blob/master/README.fr.md) + • [Schwizerdütsch readme](https://github.com/willmcgugan/rich/blob/master/README.de-ch.md) + • [हिन्दी readme](https://github.com/willmcgugan/rich/blob/master/README.hi.md) + • [Português brasileiro readme](https://github.com/willmcgugan/rich/blob/master/README.pt-br.md) + • [Italian readme](https://github.com/willmcgugan/rich/blob/master/README.it.md) + • [Русский readme](https://github.com/willmcgugan/rich/blob/master/README.ru.md) + +Rich 是一款提供終端機介面中 _豐富的_ 文字效果及精美的格式設定的 Python 函式庫。 + +[Rich API](https://rich.readthedocs.io/en/latest/) 讓終端機介面加上色彩及樣式變得易如反掌。Rich 也可以繪製漂亮的表格、進度條、Markdown、語法醒目標示的程式碼、Traceback(追溯)……。 + +![Features](https://github.com/willmcgugan/rich/raw/master/imgs/features.png) + +關於 Rich 的介紹,請參見 [@fishnets88](https://twitter.com/fishnets88) 在 [calmcode.io](https://calmcode.io/rich/introduction.html) 錄製的影片。 + +[看看其他人對於 Rich 的討論](https://www.willmcgugan.com/blog/pages/post/rich-tweets/)。 + +## 相容性 + +Rich 可在 Linux、macOS、Windows 上運作。在新的 Windows Terminal 中可支援顯示全彩及 Emoji,但傳統的終端機中僅支援 16 色。Rich 最低需要的 Python 版本為 3.6.1。 + +Rich 可在 [Jupyter notebooks](https://jupyter.org/) 上使用,無須額外設定。 + +## 安裝 + +以 `pip` 或 PyPI 套件管理器安裝。 + +```sh +python -m pip install rich +``` + +以此命令測試 Rich 在終端機的輸出效果: + +```sh +python -m rich +``` + +## Rich Print + +匯入 [rich print](https://rich.readthedocs.io/en/latest/introduction.html#quick-start) 方法就可以輕鬆地讓程式進行 rich 輸出,rich print 與 Python 內建的函式用法相似。試試: + +```python +from rich import print + +print("Hello, [bold magenta]World[/bold magenta]!", ":vampire:", locals()) +``` + +![Hello World](https://github.com/willmcgugan/rich/raw/master/imgs/print.png) + +## Rich REPL + +Rich 可以安裝在 Python REPL 中,如此一來就可以漂亮的輸出與突顯標示任何資料結構。 + +```python +>>> from rich import pretty +>>> pretty.install() +``` + +![REPL](https://github.com/willmcgugan/rich/raw/master/imgs/repl.png) + +## 使用 Console + +匯入並建構 [Console](https://rich.readthedocs.io/en/latest/reference/console.html#rich.console.Console) 物件,以更全面地控制 rich 終端機內容。 + +```python +from rich.console import Console + +console = Console() +``` + +Console 物件有個 `print` 方法,且刻意設計的與內建 `print` 函式相似。參考此範例: + +```python +console.print("Hello", "World!") +``` + +如同預期的,這會將 `"Hello World!"` 印在終端機。須注意不同於內建的 `print` 函式,Rich 會自動將過長的文字換行,以符合終端機的寬度。 + +有幾種加上顏色及樣式的方式。您可以用 `style` 引數設定輸出內容的樣式,參考此範例: + +```python +console.print("Hello", "World!", style="bold red") +``` + +輸出結果如下圖: + +![Hello World](https://github.com/willmcgugan/rich/raw/master/imgs/hello_world.png) + +介紹完了如何對整行文字設定樣式,接著來看看更細部的使用。Rich 可以接受類似 [bbcode](https://en.wikipedia.org/wiki/BBCode) 的語法,對個別文字設定樣式。參考此範例: + +```python +console.print("Where there is a [bold cyan]Will[/bold cyan] there [u]is[/u] a [i]way[/i].") +``` + +![Console Markup](https://github.com/willmcgugan/rich/raw/master/imgs/where_there_is_a_will.png) + +您可以用 Console 物件不費吹灰之力地達成細膩的輸出效果。參閱 [Console API](https://rich.readthedocs.io/en/latest/console.html) 說明文件以了解細節。 + +## Rich Inspect + +Rich 提供了 [inspect](https://rich.readthedocs.io/en/latest/reference/init.html?highlight=inspect#rich.inspect) 函式,可以對任何 Python 物件,如 class、instance 或 builtin ,為其產生一份報告。 + +```python +>>> my_list = ["foo", "bar"] +>>> from rich import inspect +>>> inspect(my_list, methods=True) +``` + +![Log](https://github.com/willmcgugan/rich/raw/master/imgs/inspect.png) + +參閱 [inspect 說明文件](https://rich.readthedocs.io/en/latest/reference/init.html#rich.inspect) 以了解細節。 + +# Rich 函式庫 + +Rich 包含了一系列可繪製的物件,您可以使用它們來印出精美的畫面,或者協助偵錯程式碼。 + +按一下子標題以了解細節: + +
+Log + +Console 物件提供了 `log()` 方法,使用方式與 `print()` 類似,但還多了一欄來顯示目前時間、進行呼叫的檔案及行號。預設情況下 Rich 會語法醒目標示 Python 的結構及 repr 字串。若使用於字典或串列這類集合性物件,Rich 會將其漂亮地印出來,以符合可用空間。此範例示範了這些功能。 + +```python +from rich.console import Console +console = Console() + +test_data = [ + {"jsonrpc": "2.0", "method": "sum", "params": [None, 1, 2, 4, False, True], "id": "1",}, + {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]}, + {"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": "2"}, +] + +def test_log(): + enabled = False + context = { + "foo": "bar", + } + movies = ["Deadpool", "Rise of the Skywalker"] + console.log("Hello from", console, "!") + console.log(test_data, log_locals=True) + + +test_log() +``` + +上面的程式碼會產生下圖結果: + +![Log](https://github.com/willmcgugan/rich/raw/master/imgs/log.png) + +注意到 `log_locals` 引數,可用來輸出一張表格,用來顯示 log 方法被呼叫時,區域變數的內容。 + +log 方法可用於伺服器上長時間運作的程式,也很適合一般程式偵錯用途。 + +
+
+Logging Handler + +您也可以使用內建的 [Handler 類別](https://rich.readthedocs.io/en/latest/logging.html) 來將 Python logging 模組的輸出內容格式化並賦予色彩: + +![Logging](https://github.com/willmcgugan/rich/raw/master/imgs/logging.png) + +
+ +
+Emoji + +以一對冒號包住表情符號的名稱,來透過 console 插入 Emoji。參考範例: + +```python +>>> console.print(":smiley: :vampire: :pile_of_poo: :thumbs_up: :raccoon:") +😃 🧛 💩 👍 🦝 +``` + +請謹慎使用此功能。 + +
+ +
+表格 + +Rich 可以用 unicode box 字元繪製彈性的 [表格](https://rich.readthedocs.io/en/latest/tables.html)。格式設定十分多元,包含框線、樣式、儲存格對齊……。 + +![table movie](https://github.com/willmcgugan/rich/raw/master/imgs/table_movie.gif) + +上圖的動畫效果是以 [table_movie.py](https://github.com/willmcgugan/rich/blob/master/examples/table_movie.py) 產生的,該檔案位於 examples 資料夾。 + +參考這個簡單的表格範例: + +```python +from rich.console import Console +from rich.table import Table + +console = Console() + +table = Table(show_header=True, header_style="bold magenta") +table.add_column("Date", style="dim", width=12) +table.add_column("Title") +table.add_column("Production Budget", justify="right") +table.add_column("Box Office", justify="right") +table.add_row( + "Dec 20, 2019", "Star Wars: The Rise of Skywalker", "$275,000,000", "$375,126,118" +) +table.add_row( + "May 25, 2018", + "[red]Solo[/red]: A Star Wars Story", + "$275,000,000", + "$393,151,347", +) +table.add_row( + "Dec 15, 2017", + "Star Wars Ep. VIII: The Last Jedi", + "$262,000,000", + "[bold]$1,332,539,889[/bold]", +) + +console.print(table) +``` + +執行結果如圖: + +![table](https://github.com/willmcgugan/rich/raw/master/imgs/table.png) + +請留意,主控台標記的呈現方式與 `print()`、`log()` 相同。事實上,由 Rich 繪製的任何東西都可以被放在任何標題、列,甚至其他表格裡。 + +`Table` 類別很聰明,能夠自動調整欄寬來配合終端機的大小,也會在需要時自動將文字換行。此範例的程式碼與上一個相同,然而終端機變小了一點: + +![table2](https://github.com/willmcgugan/rich/raw/master/imgs/table2.png) + +
+ +
+進度條 + +Rich 可繪製多個不閃爍的 [進度條](https://rich.readthedocs.io/en/latest/progress.html),以追蹤需時較久的工作。 + +基本的使用方式,是將序列放在 `track` 函式中,再對其結果疊代。參考此範例: + +```python +from rich.progress import track + +for step in track(range(100)): + do_step(step) +``` + +新增多個進度條也不是難事,來看看說明文件中的範例: + +![progress](https://github.com/willmcgugan/rich/raw/master/imgs/progress.gif) + +您可以調整要顯示的狀態欄位。內建的欄位包含完成百分比、檔案大小、讀寫速度及剩餘時間。來看看另一個用來顯示下載進度的範例: + +![progress](https://github.com/willmcgugan/rich/raw/master/imgs/downloader.gif) + +想嘗試看看嗎?您可以在 [examples/downloader.py](https://github.com/willmcgugan/rich/blob/master/examples/downloader.py) 取得此範例程式。此程式可以在下載多個檔案時顯示各自的進度。 + +
+ +
+狀態 + +有些狀況下很難估計進度,就可以使用 [status](https://rich.readthedocs.io/en/latest/reference/console.html#rich.console.Console.status) 方法,此方法會顯示「spinner」動畫及訊息。該動畫播放時,仍可正常操作主控台。參考此範例: + +```python +from time import sleep +from rich.console import Console + +console = Console() +tasks = [f"task {n}" for n in range(1, 11)] + +with console.status("[bold green]Working on tasks...") as status: + while tasks: + task = tasks.pop(0) + sleep(1) + console.log(f"{task} complete") +``` + +終端機的顯示效果如下: + +![status](https://github.com/willmcgugan/rich/raw/master/imgs/status.gif) + +該 spinner 動畫乃借用自 [cli-spinners](https://www.npmjs.com/package/cli-spinners)。可以用 `spinner` 參數指定 spinner 樣式。執行此命令以顯示可用的值: + +``` +python -m rich.spinner +``` + +此命令在終端機的輸出結果如下圖: + +![spinners](https://github.com/willmcgugan/rich/raw/master/imgs/spinners.gif) + +
+ +
+ + +Rich 可以用導引線繪製一棵 [樹](https://rich.readthedocs.io/en/latest/tree.html)。樹很適合用來顯示檔案結構,或其他繼承性的資料。 + +可以用文字或其他 Rich 能繪製的元素作為樹的標籤。執行下列程式碼來看看效果: + +``` +python -m rich.tree +``` + +這會產生下圖的結果: + +![markdown](https://github.com/willmcgugan/rich/raw/master/imgs/tree.png) + +您可以參考 [tree.py](https://github.com/willmcgugan/rich/blob/master/examples/tree.py) 範例程式,此程式可以樹狀圖展示目錄結構,如同 Linux 的 `tree` 命令。 + +
+ +
+資料欄 + +Rich 可以將內容呈現於整齊的 [資料欄](https://rich.readthedocs.io/en/latest/columns.html) 中,其欄寬可為等寬或最適寬度。此範例仿作了 macOS / Linux 系統中 `ls` 命令的基本功能,可以用資料欄列出目錄: + +```python +import os +import sys + +from rich import print +from rich.columns import Columns + +directory = os.listdir(sys.argv[1]) +print(Columns(directory)) +``` + +此螢幕截圖為 [資料欄範例](https://github.com/willmcgugan/rich/blob/master/examples/columns.py) 的輸出結果。此程式從某 API 取得資料,並以資料欄呈現: + +![columns](https://github.com/willmcgugan/rich/raw/master/imgs/columns.png) + +
+ +
+Markdown + +Rich 可以繪製 [Markdown](https://rich.readthedocs.io/en/latest/markdown.html) 並處理了將其轉換為終端機格式的大量工作。 + +先匯入 `Markdown` 類別,再以內容為 Markdown 語言的字串建構一個物件,接著將其印到 console。參考此範例: + +```python +from rich.console import Console +from rich.markdown import Markdown + +console = Console() +with open("README.md") as readme: + markdown = Markdown(readme.read()) +console.print(markdown) +``` + +執行結果如下圖: + +![markdown](https://github.com/willmcgugan/rich/raw/master/imgs/markdown.png) + +
+ +
+語法醒目標示 + +Rich 使用了 [pygments](https://pygments.org/) 函式庫來實作 [語法醒目標示](https://rich.readthedocs.io/en/latest/syntax.html) 功能。使用方式與繪製 Markdown 相似,先建構 `Syntax` 物件並將其印到 console。參考此範例: + +```python +from rich.console import Console +from rich.syntax import Syntax + +my_code = ''' +def iter_first_last(values: Iterable[T]) -> Iterable[Tuple[bool, bool, T]]: + """Iterate and generate a tuple with a flag for first and last value.""" + iter_values = iter(values) + try: + previous_value = next(iter_values) + except StopIteration: + return + first = True + for value in iter_values: + yield first, False, previous_value + first = False + previous_value = value + yield first, True, previous_value +''' +syntax = Syntax(my_code, "python", theme="monokai", line_numbers=True) +console = Console() +console.print(syntax) +``` + +執行結果如下圖: + +![syntax](https://github.com/willmcgugan/rich/raw/master/imgs/syntax.png) + +
+ +
+Tracebacks(追溯) + +Rich 可以繪製 [漂亮的 tracebacks](https://rich.readthedocs.io/en/latest/traceback.html),相較標準的 Python traceback 顯示了更多程式碼且更好懂。您可以將 Rich 設為預設的 traceback handler(處理常式),如此一來所有未接住的例外都由 Rich 呈現。 + +它在 macOS 上執行的效果如圖(Linux 上差異不大): + +![traceback](https://github.com/willmcgugan/rich/raw/master/imgs/traceback.png) + +
+ +所有可由 Rich 繪製的物件都用到了 [Console 協定](https://rich.readthedocs.io/en/latest/protocol.html),您也可以依此實作自訂的 Rich 內容。 + +# Rich 企業版 + +可在 Tidelift 訂閱方案取得。 + +Rich 及其他數以千計的套件維護者正與 Tidelift 合作,以提供開放原始碼套件的商業性支援。此計畫能協助您節省時間、避開風險,同時也讓套件的維護者獲得報酬。[了解更多。](https://tidelift.com/subscription/pkg/pypi-rich?utm_source=pypi-rich&utm_medium=referral&utm_campaign=enterprise&utm_term=repo) + +# 使用 Rich 的專案 + +以下列出幾個使用 Rich 的專案: + +- [BrancoLab/BrainRender](https://github.com/BrancoLab/BrainRender) + a python package for the visualization of three dimensional neuro-anatomical data +- [Ciphey/Ciphey](https://github.com/Ciphey/Ciphey) + Automated decryption tool +- [emeryberger/scalene](https://github.com/emeryberger/scalene) + a high-performance, high-precision CPU and memory profiler for Python +- [hedythedev/StarCli](https://github.com/hedythedev/starcli) + Browse GitHub trending projects from your command line +- [intel/cve-bin-tool](https://github.com/intel/cve-bin-tool) + This tool scans for a number of common, vulnerable components (openssl, libpng, libxml2, expat and a few others) to let you know if your system includes common libraries with known vulnerabilities. +- [nf-core/tools](https://github.com/nf-core/tools) + Python package with helper tools for the nf-core community. +- [cansarigol/pdbr](https://github.com/cansarigol/pdbr) + pdb + Rich library for enhanced debugging +- [plant99/felicette](https://github.com/plant99/felicette) + Satellite imagery for dummies. +- [seleniumbase/SeleniumBase](https://github.com/seleniumbase/SeleniumBase) + Automate & test 10x faster with Selenium & pytest. Batteries included. +- [smacke/ffsubsync](https://github.com/smacke/ffsubsync) + Automagically synchronize subtitles with video. +- [tryolabs/norfair](https://github.com/tryolabs/norfair) + Lightweight Python library for adding real-time 2D object tracking to any detector. +- [ansible/ansible-lint](https://github.com/ansible/ansible-lint) Ansible-lint checks playbooks for practices and behaviour that could potentially be improved +- [ansible-community/molecule](https://github.com/ansible-community/molecule) Ansible Molecule testing framework +- +[Many more](https://github.com/willmcgugan/rich/network/dependents)! + + diff --git a/docs/requirements.txt b/docs/requirements.txt index 849854779e..193114721b 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ alabaster==0.7.12 Sphinx==4.4.0 sphinx-rtd-theme==1.0.0 -sphinx-copybutton==0.4.0 +sphinx-copybutton==0.5.0 diff --git a/docs/source/highlighting.rst b/docs/source/highlighting.rst index 03993190c9..34274d9774 100644 --- a/docs/source/highlighting.rst +++ b/docs/source/highlighting.rst @@ -1,3 +1,5 @@ +.. _highlighting: + Highlighting ============ diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst index 57d19154b5..3576c6ac82 100644 --- a/docs/source/introduction.rst +++ b/docs/source/introduction.rst @@ -1,7 +1,7 @@ Introduction ============ -Rich is a Python library for writing *rich* text (with color and style) to the terminal, and for displaying advanced content such as tables, markdown, and syntax highlighted code. +Rich is a Python library for writing *rich* text (with color and style) to the terminal, and for displaying advanced content such as tables, markdown, and syntax highlighted code. Use Rich to make your command line applications visually appealing and present data in a more readable way. Rich can also be a useful debugging aid by pretty printing and syntax highlighting data structures. @@ -38,7 +38,7 @@ The quickest way to get up and running with Rich is to import the alternative `` from rich import print -You can then print strings or objects to the terminal in the usual way. Rich will do some basic syntax highlighting and format data structures to make them easier to read. +You can then print strings or objects to the terminal in the usual way. Rich will do some basic syntax :ref:`highlighting` and format data structures to make them easier to read. Strings may contain :ref:`console_markup` which can be used to insert color and styles in to the output. @@ -50,7 +50,7 @@ This writes the following output to the terminal (including all the colors and s .. raw:: html -
Hello World!                                                 
+    
Hello World!
     {
         '__annotations__': {},
         '__builtins__': <module 'builtins' (built-in)>,
@@ -75,7 +75,7 @@ Rich in the REPL
 Rich may be installed in the REPL so that Python data structures are automatically pretty printed with syntax highlighting. Here's how::
 
     >>> from rich import pretty
-    >>> pretty.install() 
+    >>> pretty.install()
     >>> ["Rich and pretty", True]
 
 You can also use this feature to try out Rich *renderables*. Here's an example::
@@ -91,8 +91,8 @@ IPython Extension
 Rich also includes an IPython extension that will do this same pretty install + pretty tracebacks. Here's how to load it::
 
     In [1]: %load_ext rich
-    
-You can also have it load by default by adding `"rich"` to the ``c.InteractiveShellApp.extension`` variable in 
+
+You can also have it load by default by adding `"rich"` to the ``c.InteractiveShellApp.extension`` variable in
 `IPython Configuration `_.
 
 Rich Inspect
diff --git a/docs/source/logging.rst b/docs/source/logging.rst
index 2b1d853f63..67a999922b 100644
--- a/docs/source/logging.rst
+++ b/docs/source/logging.rst
@@ -49,3 +49,23 @@ The :class:`~rich.logging.RichHandler` class may be configured to use Rich's :cl
 
 
 There are a number of other options you can use to configure logging output, see the :class:`~rich.logging.RichHandler` reference for details.
+
+Suppressing Frames
+------------------
+
+If you are working with a framework (click, django etc), you may only be interested in seeing the code from your own application within the traceback. You can exclude framework code by setting the `suppress` argument on `Traceback`, `install`, and `Console.print_exception`, which should be a list of modules or str paths.
+
+Here's how you would exclude `click `_ from Rich exceptions:: 
+
+    import click
+    import logging
+    from rich.logging import RichHandler
+
+    logging.basicConfig(
+        level="NOTSET",
+        format="%(message)s",
+        datefmt="[%X]",
+        handlers=[RichHandler(rich_tracebacks=True, tracebacks_suppress=[click])]
+    )
+
+Suppressed frames will show the line and file only, without any code.
\ No newline at end of file
diff --git a/docs/source/progress.rst b/docs/source/progress.rst
index 1a4e02ed7d..4ce3588368 100644
--- a/docs/source/progress.rst
+++ b/docs/source/progress.rst
@@ -113,12 +113,21 @@ The defaults are roughly equivalent to the following::
         TimeRemainingColumn(),
     )
 
+To create a Progress with your own columns in addition to the defaults, use :meth:`~rich.progress.Progress.get_default_columns`::
+
+    progress = Progress(
+        SpinnerColumn(),
+        *Progress.get_default_columns(),
+        TimeElapsedColumn(),
+    )
+
 The following column objects are available:
 
 - :class:`~rich.progress.BarColumn` Displays the bar.
 - :class:`~rich.progress.TextColumn` Displays text.
 - :class:`~rich.progress.TimeElapsedColumn` Displays the time elapsed.
 - :class:`~rich.progress.TimeRemainingColumn` Displays the estimated time remaining.
+- :class:`~rich.progress.MofNCompleteColumn` Displays completion progress as ``"{task.completed}/{task.total}"`` (works best if completed and total are ints).
 - :class:`~rich.progress.FileSizeColumn` Displays progress as file size (assumes the steps are bytes).
 - :class:`~rich.progress.TotalFileSizeColumn` Displays total file size (assumes the steps are bytes).
 - :class:`~rich.progress.DownloadColumn` Displays download progress (assumes the steps are bytes).
@@ -128,6 +137,7 @@ The following column objects are available:
 
 To implement your own columns, extend the :class:`~rich.progress.ProgressColumn` class and use it as you would the other columns.
 
+
 Table Columns
 ~~~~~~~~~~~~~
 
diff --git a/docs/source/style.rst b/docs/source/style.rst
index 5b8c390d58..18d4bf16ba 100644
--- a/docs/source/style.rst
+++ b/docs/source/style.rst
@@ -20,7 +20,7 @@ You may also use the color's number (an integer between 0 and 255) with the synt
 
     console.print("Hello", style="color(5)")
 
-Alteratively you can use a CSS-like syntax to specify a color with a "#" followed by three pairs of hex characters, or in RGB form with three decimal integers. The following two lines both print "Hello" in the same color (purple)::
+Alternatively you can use a CSS-like syntax to specify a color with a "#" followed by three pairs of hex characters, or in RGB form with three decimal integers. The following two lines both print "Hello" in the same color (purple)::
 
     console.print("Hello", style="#af00ff")
     console.print("Hello", style="rgb(175,0,255)")
diff --git a/docs/source/tables.rst b/docs/source/tables.rst
index 967a573825..22236358f4 100644
--- a/docs/source/tables.rst
+++ b/docs/source/tables.rst
@@ -62,14 +62,14 @@ There are a number of keyword arguments on the Table constructor you can use to
 - ``expand`` Set to True to expand the table to the full available size.
 - ``show_header`` Set to True to show a header, False to disable it.
 - ``show_footer`` Set to True to show a footer, False to disable it.
-- ``show edge`` Set to False to disable the edge line around the table.
+- ``show_edge`` Set to False to disable the edge line around the table.
 - ``show_lines`` Set to True to show lines between rows as well as header / footer.
 - ``leading`` Additional space between rows.
 - ``style`` A Style to apply to the entire table, e.g. "on blue"
 - ``row_styles`` Set to a list of styles to style alternating rows. e.g. ``["dim", ""]`` to create *zebra stripes*
 - ``header_style`` Set the default style for the header.
 - ``footer_style`` Set the default style for the footer.
-- ``border style`` Set a style for border characters.
+- ``border_style`` Set a style for border characters.
 - ``title_style`` Set a style for the title.
 - ``caption_style`` Set a style for the caption.
 - ``title_justify`` Set the title justify method ("left", "right", "center", or "full")
@@ -139,7 +139,7 @@ There are a number of options you can set on a column to modify how it will look
 - ``min_width`` When set to an integer will prevent the column from shrinking below this amount.
 - ``max_width`` When set to an integer will prevent the column from growing beyond this amount.
 - ``ratio`` Defines a ratio to set the column width. For instance, if there are 3 columns with a total of 6 ratio, and ``ratio=2`` then the column will be a third of the available size.
-- ``no_wrap`` Set to False to prevent this column from wrapping.
+- ``no_wrap`` Set to True to prevent this column from wrapping.
 
 Vertical Alignment
 ~~~~~~~~~~~~~~~~~~
diff --git a/docs/source/traceback.rst b/docs/source/traceback.rst
index f940bbbeae..6c2893222d 100644
--- a/docs/source/traceback.rst
+++ b/docs/source/traceback.rst
@@ -61,7 +61,7 @@ At this point, the traceback will be installed for any code that is run within t
 Suppressing Frames
 ------------------
 
-If you are working with a framework (click, django etc), you may only be interested in seeing the code from your own application within the traceback. You can exclude framework code by setting the `suppress` argument on `Traceback`, `install`, and `Console.print_exception`, which should be a list of modules or str paths.
+If you are working with a framework (click, django etc), you may only be interested in seeing the code from your own application within the traceback. You can exclude framework code by setting the `suppress` argument on `Traceback`, `install`, `Console.print_exception`, and `RichHandler`, which should be a list of modules or str paths.
 
 Here's how you would exclude `click `_ from Rich exceptions:: 
 
diff --git a/examples/exception.py b/examples/exception.py
index 548432d669..39dbdf4244 100644
--- a/examples/exception.py
+++ b/examples/exception.py
@@ -2,6 +2,7 @@
 Basic example to show how to print an traceback of an exception
 """
 from typing import List, Tuple
+
 from rich.console import Console
 
 console = Console()
@@ -34,7 +35,7 @@ def divide_all(divides: List[Tuple[float, float]]) -> None:
     (0, 1000000),
     (3.1427, 2),
     (888, 0),
-    (2 ** 32, 2 ** 16),
+    (2**32, 2**16),
 ]
 
 divide_all(DIVIDES)
diff --git a/imgs/downloader.gif b/imgs/downloader.gif
index 2033d7b981..dad59f2588 100644
Binary files a/imgs/downloader.gif and b/imgs/downloader.gif differ
diff --git a/imgs/logo.svg b/imgs/logo.svg
index 34c2a11ead..4842c4a8ef 100644
--- a/imgs/logo.svg
+++ b/imgs/logo.svg
@@ -1,69 +1 @@
-
-
-  
-    
-      
-      
-    
-    
-      
-      
-      
-      
-        
-        
-      
-    
-  
-  
-    
-    
-    
-    
-    
-    
-    
-      
-      
-      
-      
-      
-      
-      
-      
-      
-      
-      
-      
-      
-      
-      
-      
-      
-      
-      
-      
-      
-      
-      
-      
-      
-      
-      
-      
-      
-      
-      
-    
-  
-  
-    
-      
-      
-      
-      
-      
-    
-  
-  
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/imgs/progress.gif b/imgs/progress.gif
index e145478fa9..b4ccfa2f4b 100644
Binary files a/imgs/progress.gif and b/imgs/progress.gif differ
diff --git a/imgs/spinners.gif b/imgs/spinners.gif
index b98685c203..7d3445e104 100644
Binary files a/imgs/spinners.gif and b/imgs/spinners.gif differ
diff --git a/imgs/status.gif b/imgs/status.gif
index ccb7ceb8e7..c930fc76ef 100644
Binary files a/imgs/status.gif and b/imgs/status.gif differ
diff --git a/imgs/traceback.png b/imgs/traceback.png
index 55b2e79a0b..07a4a50076 100644
Binary files a/imgs/traceback.png and b/imgs/traceback.png differ
diff --git a/mypy.ini b/mypy.ini
index c61bcf0ace..acccf08251 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -9,8 +9,5 @@ ignore_missing_imports = True
 [mypy-commonmark.*]
 ignore_missing_imports = True
 
-[mypy-colorama.*]
-ignore_missing_imports = True
-
 [mypy-ipywidgets.*]
 ignore_missing_imports = True
diff --git a/poetry.lock b/poetry.lock
index 59c3236d6d..0489808c65 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -3,7 +3,7 @@ name = "appnope"
 version = "0.1.2"
 description = "Disable App Nap on macOS >= 10.9"
 category = "main"
-optional = false
+optional = true
 python-versions = "*"
 
 [[package]]
@@ -74,35 +74,31 @@ name = "backcall"
 version = "0.2.0"
 description = "Specifications for callback functions passed in to an API"
 category = "main"
-optional = false
+optional = true
 python-versions = "*"
 
 [[package]]
 name = "black"
-version = "21.12b0"
+version = "22.1.0"
 description = "The uncompromising code formatter."
 category = "dev"
 optional = false
 python-versions = ">=3.6.2"
 
 [package.dependencies]
-click = ">=7.1.2"
+click = ">=8.0.0"
 dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""}
 mypy-extensions = ">=0.4.3"
-pathspec = ">=0.9.0,<1"
+pathspec = ">=0.9.0"
 platformdirs = ">=2"
-tomli = ">=0.2.6,<2.0.0"
+tomli = ">=1.1.0"
 typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""}
-typing-extensions = [
-    {version = ">=3.10.0.0", markers = "python_version < \"3.10\""},
-    {version = "!=3.10.0.1", markers = "python_version >= \"3.10\""},
-]
+typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
 
 [package.extras]
 colorama = ["colorama (>=0.4.3)"]
 d = ["aiohttp (>=3.7.4)"]
 jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
-python2 = ["typed-ast (>=1.4.3)"]
 uvloop = ["uvloop (>=0.15.2)"]
 
 [[package]]
@@ -139,7 +135,7 @@ python-versions = ">=3.6.1"
 
 [[package]]
 name = "click"
-version = "8.0.3"
+version = "8.0.4"
 description = "Composable command line interface toolkit"
 category = "dev"
 optional = false
@@ -192,10 +188,10 @@ python-versions = ">=3.6, <3.7"
 
 [[package]]
 name = "decorator"
-version = "5.1.0"
+version = "5.1.1"
 description = "Decorators for Humans"
 category = "main"
-optional = false
+optional = true
 python-versions = ">=3.5"
 
 [[package]]
@@ -216,11 +212,11 @@ python-versions = "*"
 
 [[package]]
 name = "entrypoints"
-version = "0.3"
+version = "0.4"
 description = "Discover and load entry points from installed packages."
 category = "main"
 optional = true
-python-versions = ">=2.7"
+python-versions = ">=3.6"
 
 [[package]]
 name = "filelock"
@@ -236,7 +232,7 @@ testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-co
 
 [[package]]
 name = "identify"
-version = "2.4.1"
+version = "2.4.4"
 description = "File identification library for Python"
 category = "dev"
 optional = false
@@ -247,7 +243,7 @@ license = ["ukkonen"]
 
 [[package]]
 name = "importlib-metadata"
-version = "4.8.2"
+version = "4.8.3"
 description = "Read metadata from Python packages"
 category = "main"
 optional = false
@@ -309,7 +305,7 @@ name = "ipython"
 version = "7.16.3"
 description = "IPython: Productive Interactive Computing"
 category = "main"
-optional = false
+optional = true
 python-versions = ">=3.6"
 
 [package.dependencies]
@@ -340,7 +336,7 @@ name = "ipython-genutils"
 version = "0.2.0"
 description = "Vestigial utilities from IPython"
 category = "main"
-optional = false
+optional = true
 python-versions = "*"
 
 [[package]]
@@ -368,7 +364,7 @@ name = "jedi"
 version = "0.17.2"
 description = "An autocompletion tool for Python that can be used for text editors."
 category = "main"
-optional = false
+optional = true
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 
 [package.dependencies]
@@ -411,7 +407,7 @@ format_nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-
 
 [[package]]
 name = "jupyter-client"
-version = "7.1.0"
+version = "7.1.2"
 description = "Jupyter protocol implementation and client libraries"
 category = "main"
 optional = true
@@ -432,7 +428,7 @@ test = ["codecov", "coverage", "ipykernel", "ipython", "mock", "mypy", "pre-comm
 
 [[package]]
 name = "jupyter-core"
-version = "4.9.1"
+version = "4.9.2"
 description = "Jupyter core package. A base package on which Jupyter projects rely."
 category = "main"
 optional = true
@@ -479,7 +475,7 @@ python-versions = "*"
 
 [[package]]
 name = "mypy"
-version = "0.930"
+version = "0.931"
 description = "Optional static typing for Python"
 category = "dev"
 optional = false
@@ -589,7 +585,7 @@ python-versions = "*"
 
 [[package]]
 name = "notebook"
-version = "6.4.6"
+version = "6.4.8"
 description = "A web-based notebook environment for interactive computing"
 category = "main"
 optional = true
@@ -641,7 +637,7 @@ name = "parso"
 version = "0.7.1"
 description = "A Python Parser"
 category = "main"
-optional = false
+optional = true
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 
 [package.extras]
@@ -660,7 +656,7 @@ name = "pexpect"
 version = "4.8.0"
 description = "Pexpect allows easy control of interactive console applications."
 category = "main"
-optional = false
+optional = true
 python-versions = "*"
 
 [package.dependencies]
@@ -671,7 +667,7 @@ name = "pickleshare"
 version = "0.7.5"
 description = "Tiny 'shelve'-like database with concurrency support"
 category = "main"
-optional = false
+optional = true
 python-versions = "*"
 
 [[package]]
@@ -703,7 +699,7 @@ testing = ["pytest", "pytest-benchmark"]
 
 [[package]]
 name = "pre-commit"
-version = "2.16.0"
+version = "2.17.0"
 description = "A framework for managing and maintaining multi-language pre-commit hooks."
 category = "dev"
 optional = false
@@ -721,21 +717,21 @@ virtualenv = ">=20.0.8"
 
 [[package]]
 name = "prometheus-client"
-version = "0.12.0"
+version = "0.13.1"
 description = "Python client for the Prometheus monitoring system."
 category = "main"
 optional = true
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+python-versions = ">=3.6"
 
 [package.extras]
 twisted = ["twisted"]
 
 [[package]]
 name = "prompt-toolkit"
-version = "3.0.24"
+version = "3.0.28"
 description = "Library for building powerful interactive command lines in Python"
 category = "main"
-optional = false
+optional = true
 python-versions = ">=3.6.2"
 
 [package.dependencies]
@@ -746,7 +742,7 @@ name = "ptyprocess"
 version = "0.7.0"
 description = "Run a subprocess in a pseudo terminal"
 category = "main"
-optional = false
+optional = true
 python-versions = "*"
 
 [[package]]
@@ -775,7 +771,7 @@ python-versions = ">=3.5"
 
 [[package]]
 name = "pyparsing"
-version = "3.0.6"
+version = "3.0.7"
 description = "Python parsing module"
 category = "main"
 optional = false
@@ -794,7 +790,7 @@ python-versions = ">=3.6"
 
 [[package]]
 name = "pytest"
-version = "6.2.5"
+version = "7.0.1"
 description = "pytest: simple powerful testing with Python"
 category = "dev"
 optional = false
@@ -809,10 +805,10 @@ iniconfig = "*"
 packaging = "*"
 pluggy = ">=0.12,<2.0"
 py = ">=1.8.2"
-toml = "*"
+tomli = ">=1.0.0"
 
 [package.extras]
-testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
+testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
 
 [[package]]
 name = "pytest-cov"
@@ -842,7 +838,7 @@ six = ">=1.5"
 
 [[package]]
 name = "pywin32"
-version = "302"
+version = "303"
 description = "Python for Window Extensions"
 category = "main"
 optional = true
@@ -850,7 +846,7 @@ python-versions = "*"
 
 [[package]]
 name = "pywinpty"
-version = "1.1.6"
+version = "2.0.3"
 description = "Pseudo terminal support for Windows from Python."
 category = "main"
 optional = true
@@ -899,11 +895,11 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
 
 [[package]]
 name = "terminado"
-version = "0.12.1"
+version = "0.13.0"
 description = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library."
 category = "main"
 optional = true
-python-versions = ">=3.6"
+python-versions = "*"
 
 [package.dependencies]
 ptyprocess = {version = "*", markers = "os_name != \"nt\""}
@@ -915,14 +911,14 @@ test = ["pytest"]
 
 [[package]]
 name = "testpath"
-version = "0.5.0"
+version = "0.6.0"
 description = "Test utilities for code working with files and commands"
 category = "main"
 optional = true
 python-versions = ">= 3.5"
 
 [package.extras]
-test = ["pytest", "pathlib2"]
+test = ["pytest"]
 
 [[package]]
 name = "toml"
@@ -934,7 +930,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
 
 [[package]]
 name = "tomli"
-version = "1.2.2"
+version = "1.2.3"
 description = "A lil' TOML parser"
 category = "dev"
 optional = false
@@ -953,7 +949,7 @@ name = "traitlets"
 version = "4.3.3"
 description = "Traitlets Python config system"
 category = "main"
-optional = false
+optional = true
 python-versions = "*"
 
 [package.dependencies]
@@ -966,11 +962,11 @@ test = ["pytest", "mock"]
 
 [[package]]
 name = "typed-ast"
-version = "1.4.3"
+version = "1.5.2"
 description = "a fork of Python 2 and 3 ast modules with type comment support"
 category = "dev"
 optional = false
-python-versions = "*"
+python-versions = ">=3.6"
 
 [[package]]
 name = "types-dataclasses"
@@ -982,7 +978,7 @@ python-versions = "*"
 
 [[package]]
 name = "typing-extensions"
-version = "4.0.1"
+version = "4.1.1"
 description = "Backported and Experimental Type Hints for Python 3.6+"
 category = "main"
 optional = false
@@ -990,7 +986,7 @@ python-versions = ">=3.6"
 
 [[package]]
 name = "virtualenv"
-version = "20.13.0"
+version = "20.13.3"
 description = "Virtual Python Environment builder"
 category = "dev"
 optional = false
@@ -1013,7 +1009,7 @@ name = "wcwidth"
 version = "0.2.5"
 description = "Measures the displayed width of unicode strings in a terminal"
 category = "main"
-optional = false
+optional = true
 python-versions = "*"
 
 [[package]]
@@ -1053,7 +1049,7 @@ jupyter = ["ipywidgets"]
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.6.2"
-content-hash = "d642a2a33643ec67d8ec48fef8ac01b7226829d0dcc3a14b10c3140005cbd0f0"
+content-hash = "05eba8d679f7fd74e1fb50881b99c09705bea9330a70beeead174c5b8fe6338c"
 
 [metadata.files]
 appnope = [
@@ -1104,8 +1100,29 @@ backcall = [
     {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
 ]
 black = [
-    {file = "black-21.12b0-py3-none-any.whl", hash = "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f"},
-    {file = "black-21.12b0.tar.gz", hash = "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3"},
+    {file = "black-22.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1297c63b9e1b96a3d0da2d85d11cd9bf8664251fd69ddac068b98dc4f34f73b6"},
+    {file = "black-22.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2ff96450d3ad9ea499fc4c60e425a1439c2120cbbc1ab959ff20f7c76ec7e866"},
+    {file = "black-22.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e21e1f1efa65a50e3960edd068b6ae6d64ad6235bd8bfea116a03b21836af71"},
+    {file = "black-22.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f69158a7d120fd641d1fa9a921d898e20d52e44a74a6fbbcc570a62a6bc8ab"},
+    {file = "black-22.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:228b5ae2c8e3d6227e4bde5920d2fc66cc3400fde7bcc74f480cb07ef0b570d5"},
+    {file = "black-22.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b1a5ed73ab4c482208d20434f700d514f66ffe2840f63a6252ecc43a9bc77e8a"},
+    {file = "black-22.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35944b7100af4a985abfcaa860b06af15590deb1f392f06c8683b4381e8eeaf0"},
+    {file = "black-22.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7835fee5238fc0a0baf6c9268fb816b5f5cd9b8793423a75e8cd663c48d073ba"},
+    {file = "black-22.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dae63f2dbf82882fa3b2a3c49c32bffe144970a573cd68d247af6560fc493ae1"},
+    {file = "black-22.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa1db02410b1924b6749c245ab38d30621564e658297484952f3d8a39fce7e8"},
+    {file = "black-22.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c8226f50b8c34a14608b848dc23a46e5d08397d009446353dad45e04af0c8e28"},
+    {file = "black-22.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2d6f331c02f0f40aa51a22e479c8209d37fcd520c77721c034517d44eecf5912"},
+    {file = "black-22.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:742ce9af3086e5bd07e58c8feb09dbb2b047b7f566eb5f5bc63fd455814979f3"},
+    {file = "black-22.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fdb8754b453fb15fad3f72cd9cad3e16776f0964d67cf30ebcbf10327a3777a3"},
+    {file = "black-22.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5660feab44c2e3cb24b2419b998846cbb01c23c7fe645fee45087efa3da2d61"},
+    {file = "black-22.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:6f2f01381f91c1efb1451998bd65a129b3ed6f64f79663a55fe0e9b74a5f81fd"},
+    {file = "black-22.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:efbadd9b52c060a8fc3b9658744091cb33c31f830b3f074422ed27bad2b18e8f"},
+    {file = "black-22.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8871fcb4b447206904932b54b567923e5be802b9b19b744fdff092bd2f3118d0"},
+    {file = "black-22.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccad888050f5393f0d6029deea2a33e5ae371fd182a697313bdbd835d3edaf9c"},
+    {file = "black-22.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07e5c049442d7ca1a2fc273c79d1aecbbf1bc858f62e8184abe1ad175c4f7cc2"},
+    {file = "black-22.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:373922fc66676133ddc3e754e4509196a8c392fec3f5ca4486673e685a421321"},
+    {file = "black-22.1.0-py3-none-any.whl", hash = "sha256:3524739d76b6b3ed1132422bf9d82123cd1705086723bc3e235ca39fd21c667d"},
+    {file = "black-22.1.0.tar.gz", hash = "sha256:a7c0192d35635f6fc1174be575cb7915e92e5dd629ee79fdaf0dcfa41a80afb5"},
 ]
 bleach = [
     {file = "bleach-4.1.0-py2.py3-none-any.whl", hash = "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994"},
@@ -1168,8 +1185,8 @@ cfgv = [
     {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
 ]
 click = [
-    {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"},
-    {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"},
+    {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"},
+    {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"},
 ]
 colorama = [
     {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
@@ -1233,8 +1250,8 @@ dataclasses = [
     {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"},
 ]
 decorator = [
-    {file = "decorator-5.1.0-py3-none-any.whl", hash = "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374"},
-    {file = "decorator-5.1.0.tar.gz", hash = "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7"},
+    {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
+    {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
 ]
 defusedxml = [
     {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
@@ -1245,20 +1262,20 @@ distlib = [
     {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"},
 ]
 entrypoints = [
-    {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"},
-    {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"},
+    {file = "entrypoints-0.4-py3-none-any.whl", hash = "sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f"},
+    {file = "entrypoints-0.4.tar.gz", hash = "sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4"},
 ]
 filelock = [
     {file = "filelock-3.4.1-py3-none-any.whl", hash = "sha256:a4bc51381e01502a30e9f06dd4fa19a1712eab852b6fb0f84fd7cce0793d8ca3"},
     {file = "filelock-3.4.1.tar.gz", hash = "sha256:0f12f552b42b5bf60dba233710bf71337d35494fc8bdd4fd6d9f6d082ad45e06"},
 ]
 identify = [
-    {file = "identify-2.4.1-py2.py3-none-any.whl", hash = "sha256:0192893ff68b03d37fed553e261d4a22f94ea974093aefb33b29df2ff35fed3c"},
-    {file = "identify-2.4.1.tar.gz", hash = "sha256:64d4885e539f505dd8ffb5e93c142a1db45480452b1594cacd3e91dca9a984e9"},
+    {file = "identify-2.4.4-py2.py3-none-any.whl", hash = "sha256:aa68609c7454dbcaae60a01ff6b8df1de9b39fe6e50b1f6107ec81dcda624aa6"},
+    {file = "identify-2.4.4.tar.gz", hash = "sha256:6b4b5031f69c48bf93a646b90de9b381c6b5f560df4cbe0ed3cf7650ae741e4d"},
 ]
 importlib-metadata = [
-    {file = "importlib_metadata-4.8.2-py3-none-any.whl", hash = "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100"},
-    {file = "importlib_metadata-4.8.2.tar.gz", hash = "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb"},
+    {file = "importlib_metadata-4.8.3-py3-none-any.whl", hash = "sha256:65a9576a5b2d58ca44d133c42a241905cc45e34d2c06fd5ba2bafa221e5d7b5e"},
+    {file = "importlib_metadata-4.8.3.tar.gz", hash = "sha256:766abffff765960fcc18003801f7044eb6755ffae4521c8e8ce8e83b9c9b0668"},
 ]
 importlib-resources = [
     {file = "importlib_resources-5.2.3-py3-none-any.whl", hash = "sha256:ae35ed1cfe8c0d6c1a53ecd168167f01fa93b893d51a62cdf23aea044c67211b"},
@@ -1297,12 +1314,12 @@ jsonschema = [
     {file = "jsonschema-4.0.0.tar.gz", hash = "sha256:bc51325b929171791c42ebc1c70b9713eb134d3bb8ebd5474c8b659b15be6d86"},
 ]
 jupyter-client = [
-    {file = "jupyter_client-7.1.0-py3-none-any.whl", hash = "sha256:64d93752d8cbfba0c1030c3335c3f0d9797cd1efac012652a14aac1653db11a3"},
-    {file = "jupyter_client-7.1.0.tar.gz", hash = "sha256:a5f995a73cffb314ed262713ae6dfce53c6b8216cea9f332071b8ff44a6e1654"},
+    {file = "jupyter_client-7.1.2-py3-none-any.whl", hash = "sha256:d56f1c57bef42ff31e61b1185d3348a5b2bcde7c9a05523ae4dbe5ee0871797c"},
+    {file = "jupyter_client-7.1.2.tar.gz", hash = "sha256:4ea61033726c8e579edb55626d8ee2e6bf0a83158ddf3751b8dd46b2c5cd1e96"},
 ]
 jupyter-core = [
-    {file = "jupyter_core-4.9.1-py3-none-any.whl", hash = "sha256:1c091f3bbefd6f2a8782f2c1db662ca8478ac240e962ae2c66f0b87c818154ea"},
-    {file = "jupyter_core-4.9.1.tar.gz", hash = "sha256:dce8a7499da5a53ae3afd5a9f4b02e5df1d57250cf48f3ad79da23b4778cd6fa"},
+    {file = "jupyter_core-4.9.2-py3-none-any.whl", hash = "sha256:f875e4d27e202590311d468fa55f90c575f201490bd0c18acabe4e318db4a46d"},
+    {file = "jupyter_core-4.9.2.tar.gz", hash = "sha256:d69baeb9ffb128b8cd2657fcf2703f89c769d1673c851812119e3a2a0e93ad9a"},
 ]
 jupyterlab-pygments = [
     {file = "jupyterlab_pygments-0.1.2-py2.py3-none-any.whl", hash = "sha256:abfb880fd1561987efaefcb2d2ac75145d2a5d0139b1876d5be806e32f630008"},
@@ -1388,26 +1405,26 @@ mistune = [
     {file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"},
 ]
 mypy = [
-    {file = "mypy-0.930-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:221cc94dc6a801ccc2be7c0c9fd791c5e08d1fa2c5e1c12dec4eab15b2469871"},
-    {file = "mypy-0.930-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db3a87376a1380f396d465bed462e76ea89f838f4c5e967d68ff6ee34b785c31"},
-    {file = "mypy-0.930-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1d2296f35aae9802eeb1327058b550371ee382d71374b3e7d2804035ef0b830b"},
-    {file = "mypy-0.930-cp310-cp310-win_amd64.whl", hash = "sha256:959319b9a3cafc33a8185f440a433ba520239c72e733bf91f9efd67b0a8e9b30"},
-    {file = "mypy-0.930-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:45a4dc21c789cfd09b8ccafe114d6de66f0b341ad761338de717192f19397a8c"},
-    {file = "mypy-0.930-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1e689e92cdebd87607a041585f1dc7339aa2e8a9f9bad9ba7e6ece619431b20c"},
-    {file = "mypy-0.930-cp36-cp36m-win_amd64.whl", hash = "sha256:ed4e0ea066bb12f56b2812a15ff223c57c0a44eca817ceb96b214bb055c7051f"},
-    {file = "mypy-0.930-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a9d8dffefba634b27d650e0de2564379a1a367e2e08d6617d8f89261a3bf63b2"},
-    {file = "mypy-0.930-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b419e9721260161e70d054a15abbd50603c16f159860cfd0daeab647d828fc29"},
-    {file = "mypy-0.930-cp37-cp37m-win_amd64.whl", hash = "sha256:601f46593f627f8a9b944f74fd387c9b5f4266b39abad77471947069c2fc7651"},
-    {file = "mypy-0.930-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1ea7199780c1d7940b82dbc0a4e37722b4e3851264dbba81e01abecc9052d8a7"},
-    {file = "mypy-0.930-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:70b197dd8c78fc5d2daf84bd093e8466a2b2e007eedaa85e792e513a820adbf7"},
-    {file = "mypy-0.930-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5feb56f8bb280468fe5fc8e6f56f48f99aa0df9eed3c507a11505ee4657b5380"},
-    {file = "mypy-0.930-cp38-cp38-win_amd64.whl", hash = "sha256:2e9c5409e9cb81049bb03fa1009b573dea87976713e3898561567a86c4eaee01"},
-    {file = "mypy-0.930-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:554873e45c1ca20f31ddf873deb67fa5d2e87b76b97db50669f0468ccded8fae"},
-    {file = "mypy-0.930-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0feb82e9fa849affca7edd24713dbe809dce780ced9f3feca5ed3d80e40b777f"},
-    {file = "mypy-0.930-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bc1a0607ea03c30225347334af66b0af12eefba018a89a88c209e02b7065ea95"},
-    {file = "mypy-0.930-cp39-cp39-win_amd64.whl", hash = "sha256:f9f665d69034b1fcfdbcd4197480d26298bbfb5d2dfe206245b6498addb34999"},
-    {file = "mypy-0.930-py3-none-any.whl", hash = "sha256:bf4a44e03040206f7c058d1f5ba02ef2d1820720c88bc4285c7d9a4269f54173"},
-    {file = "mypy-0.930.tar.gz", hash = "sha256:51426262ae4714cc7dd5439814676e0992b55bcc0f6514eccb4cf8e0678962c2"},
+    {file = "mypy-0.931-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3c5b42d0815e15518b1f0990cff7a705805961613e701db60387e6fb663fe78a"},
+    {file = "mypy-0.931-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c89702cac5b302f0c5d33b172d2b55b5df2bede3344a2fbed99ff96bddb2cf00"},
+    {file = "mypy-0.931-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:300717a07ad09525401a508ef5d105e6b56646f7942eb92715a1c8d610149714"},
+    {file = "mypy-0.931-cp310-cp310-win_amd64.whl", hash = "sha256:7b3f6f557ba4afc7f2ce6d3215d5db279bcf120b3cfd0add20a5d4f4abdae5bc"},
+    {file = "mypy-0.931-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1bf752559797c897cdd2c65f7b60c2b6969ffe458417b8d947b8340cc9cec08d"},
+    {file = "mypy-0.931-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4365c60266b95a3f216a3047f1d8e3f895da6c7402e9e1ddfab96393122cc58d"},
+    {file = "mypy-0.931-cp36-cp36m-win_amd64.whl", hash = "sha256:1b65714dc296a7991000b6ee59a35b3f550e0073411ac9d3202f6516621ba66c"},
+    {file = "mypy-0.931-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e839191b8da5b4e5d805f940537efcaa13ea5dd98418f06dc585d2891d228cf0"},
+    {file = "mypy-0.931-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:50c7346a46dc76a4ed88f3277d4959de8a2bd0a0fa47fa87a4cde36fe247ac05"},
+    {file = "mypy-0.931-cp37-cp37m-win_amd64.whl", hash = "sha256:d8f1ff62f7a879c9fe5917b3f9eb93a79b78aad47b533911b853a757223f72e7"},
+    {file = "mypy-0.931-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9fe20d0872b26c4bba1c1be02c5340de1019530302cf2dcc85c7f9fc3252ae0"},
+    {file = "mypy-0.931-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1b06268df7eb53a8feea99cbfff77a6e2b205e70bf31743e786678ef87ee8069"},
+    {file = "mypy-0.931-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8c11003aaeaf7cc2d0f1bc101c1cc9454ec4cc9cb825aef3cafff8a5fdf4c799"},
+    {file = "mypy-0.931-cp38-cp38-win_amd64.whl", hash = "sha256:d9d2b84b2007cea426e327d2483238f040c49405a6bf4074f605f0156c91a47a"},
+    {file = "mypy-0.931-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ff3bf387c14c805ab1388185dd22d6b210824e164d4bb324b195ff34e322d166"},
+    {file = "mypy-0.931-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b56154f8c09427bae082b32275a21f500b24d93c88d69a5e82f3978018a0266"},
+    {file = "mypy-0.931-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8ca7f8c4b1584d63c9a0f827c37ba7a47226c19a23a753d52e5b5eddb201afcd"},
+    {file = "mypy-0.931-cp39-cp39-win_amd64.whl", hash = "sha256:74f7eccbfd436abe9c352ad9fb65872cc0f1f0a868e9d9c44db0893440f0c697"},
+    {file = "mypy-0.931-py3-none-any.whl", hash = "sha256:1171f2e0859cfff2d366da2c7092b06130f232c636a3f7301e3feb8b41f6377d"},
+    {file = "mypy-0.931.tar.gz", hash = "sha256:0038b21890867793581e4cb0d810829f5fd4441aa75796b53033af3aa30430ce"},
 ]
 mypy-extensions = [
     {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
@@ -1434,8 +1451,8 @@ nodeenv = [
     {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"},
 ]
 notebook = [
-    {file = "notebook-6.4.6-py3-none-any.whl", hash = "sha256:5cad068fa82cd4fb98d341c052100ed50cd69fbfb4118cb9b8ab5a346ef27551"},
-    {file = "notebook-6.4.6.tar.gz", hash = "sha256:7bcdf79bd1cda534735bd9830d2cbedab4ee34d8fe1df6e7b946b3aab0902ba3"},
+    {file = "notebook-6.4.8-py3-none-any.whl", hash = "sha256:3e702fcc54b8ae597533c3864793b7a1e971dec9e112f67235828d8a798fd654"},
+    {file = "notebook-6.4.8.tar.gz", hash = "sha256:1e985c9dc6f678bdfffb9dc657306b5469bfa62d73e03f74e8defbf76d284312"},
 ]
 packaging = [
     {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
@@ -1470,16 +1487,16 @@ pluggy = [
     {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
 ]
 pre-commit = [
-    {file = "pre_commit-2.16.0-py2.py3-none-any.whl", hash = "sha256:758d1dc9b62c2ed8881585c254976d66eae0889919ab9b859064fc2fe3c7743e"},
-    {file = "pre_commit-2.16.0.tar.gz", hash = "sha256:fe9897cac830aa7164dbd02a4e7b90cae49630451ce88464bca73db486ba9f65"},
+    {file = "pre_commit-2.17.0-py2.py3-none-any.whl", hash = "sha256:725fa7459782d7bec5ead072810e47351de01709be838c2ce1726b9591dad616"},
+    {file = "pre_commit-2.17.0.tar.gz", hash = "sha256:c1a8040ff15ad3d648c70cc3e55b93e4d2d5b687320955505587fd79bbaed06a"},
 ]
 prometheus-client = [
-    {file = "prometheus_client-0.12.0-py2.py3-none-any.whl", hash = "sha256:317453ebabff0a1b02df7f708efbab21e3489e7072b61cb6957230dd004a0af0"},
-    {file = "prometheus_client-0.12.0.tar.gz", hash = "sha256:1b12ba48cee33b9b0b9de64a1047cbd3c5f2d0ab6ebcead7ddda613a750ec3c5"},
+    {file = "prometheus_client-0.13.1-py3-none-any.whl", hash = "sha256:357a447fd2359b0a1d2e9b311a0c5778c330cfbe186d880ad5a6b39884652316"},
+    {file = "prometheus_client-0.13.1.tar.gz", hash = "sha256:ada41b891b79fca5638bd5cfe149efa86512eaa55987893becd2c6d8d0a5dfc5"},
 ]
 prompt-toolkit = [
-    {file = "prompt_toolkit-3.0.24-py3-none-any.whl", hash = "sha256:e56f2ff799bacecd3e88165b1e2f5ebf9bcd59e80e06d395fa0cc4b8bd7bb506"},
-    {file = "prompt_toolkit-3.0.24.tar.gz", hash = "sha256:1bb05628c7d87b645974a1bad3f17612be0c29fa39af9f7688030163f680bad6"},
+    {file = "prompt_toolkit-3.0.28-py3-none-any.whl", hash = "sha256:30129d870dcb0b3b6a53efdc9d0a83ea96162ffd28ffe077e94215b233dc670c"},
+    {file = "prompt_toolkit-3.0.28.tar.gz", hash = "sha256:9f1cd16b1e86c2968f2519d7fb31dd9d669916f515612c269d14e9ed52b51650"},
 ]
 ptyprocess = [
     {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
@@ -1498,8 +1515,8 @@ pygments = [
     {file = "Pygments-2.11.2.tar.gz", hash = "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"},
 ]
 pyparsing = [
-    {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"},
-    {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"},
+    {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"},
+    {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"},
 ]
 pyrsistent = [
     {file = "pyrsistent-0.18.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f4c8cabb46ff8e5d61f56a037974228e978f26bfefce4f61a4b1ac0ba7a2ab72"},
@@ -1525,8 +1542,8 @@ pyrsistent = [
     {file = "pyrsistent-0.18.0.tar.gz", hash = "sha256:773c781216f8c2900b42a7b638d5b517bb134ae1acbebe4d1e8f1f41ea60eb4b"},
 ]
 pytest = [
-    {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
-    {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
+    {file = "pytest-7.0.1-py3-none-any.whl", hash = "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db"},
+    {file = "pytest-7.0.1.tar.gz", hash = "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"},
 ]
 pytest-cov = [
     {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"},
@@ -1537,24 +1554,25 @@ python-dateutil = [
     {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
 ]
 pywin32 = [
-    {file = "pywin32-302-cp310-cp310-win32.whl", hash = "sha256:251b7a9367355ccd1a4cd69cd8dd24bd57b29ad83edb2957cfa30f7ed9941efa"},
-    {file = "pywin32-302-cp310-cp310-win_amd64.whl", hash = "sha256:79cf7e6ddaaf1cd47a9e50cc74b5d770801a9db6594464137b1b86aa91edafcc"},
-    {file = "pywin32-302-cp36-cp36m-win32.whl", hash = "sha256:fe21c2fb332d03dac29de070f191bdbf14095167f8f2165fdc57db59b1ecc006"},
-    {file = "pywin32-302-cp36-cp36m-win_amd64.whl", hash = "sha256:d3761ab4e8c5c2dbc156e2c9ccf38dd51f936dc77e58deb940ffbc4b82a30528"},
-    {file = "pywin32-302-cp37-cp37m-win32.whl", hash = "sha256:48dd4e348f1ee9538dd4440bf201ea8c110ea6d9f3a5010d79452e9fa80480d9"},
-    {file = "pywin32-302-cp37-cp37m-win_amd64.whl", hash = "sha256:496df89f10c054c9285cc99f9d509e243f4e14ec8dfc6d78c9f0bf147a893ab1"},
-    {file = "pywin32-302-cp38-cp38-win32.whl", hash = "sha256:e372e477d938a49266136bff78279ed14445e00718b6c75543334351bf535259"},
-    {file = "pywin32-302-cp38-cp38-win_amd64.whl", hash = "sha256:543552e66936378bd2d673c5a0a3d9903dba0b0a87235ef0c584f058ceef5872"},
-    {file = "pywin32-302-cp39-cp39-win32.whl", hash = "sha256:2393c1a40dc4497fd6161b76801b8acd727c5610167762b7c3e9fd058ef4a6ab"},
-    {file = "pywin32-302-cp39-cp39-win_amd64.whl", hash = "sha256:af5aea18167a31efcacc9f98a2ca932c6b6a6d91ebe31f007509e293dea12580"},
+    {file = "pywin32-303-cp310-cp310-win32.whl", hash = "sha256:6fed4af057039f309263fd3285d7b8042d41507343cd5fa781d98fcc5b90e8bb"},
+    {file = "pywin32-303-cp310-cp310-win_amd64.whl", hash = "sha256:51cb52c5ec6709f96c3f26e7795b0bf169ee0d8395b2c1d7eb2c029a5008ed51"},
+    {file = "pywin32-303-cp311-cp311-win32.whl", hash = "sha256:d9b5d87ca944eb3aa4cd45516203ead4b37ab06b8b777c54aedc35975dec0dee"},
+    {file = "pywin32-303-cp311-cp311-win_amd64.whl", hash = "sha256:fcf44032f5b14fcda86028cdf49b6ebdaea091230eb0a757282aa656e4732439"},
+    {file = "pywin32-303-cp36-cp36m-win32.whl", hash = "sha256:aad484d52ec58008ca36bd4ad14a71d7dd0a99db1a4ca71072213f63bf49c7d9"},
+    {file = "pywin32-303-cp36-cp36m-win_amd64.whl", hash = "sha256:2a09632916b6bb231ba49983fe989f2f625cea237219530e81a69239cd0c4559"},
+    {file = "pywin32-303-cp37-cp37m-win32.whl", hash = "sha256:b1675d82bcf6dbc96363fca747bac8bff6f6e4a447a4287ac652aa4b9adc796e"},
+    {file = "pywin32-303-cp37-cp37m-win_amd64.whl", hash = "sha256:c268040769b48a13367221fced6d4232ed52f044ffafeda247bd9d2c6bdc29ca"},
+    {file = "pywin32-303-cp38-cp38-win32.whl", hash = "sha256:5f9ec054f5a46a0f4dfd72af2ce1372f3d5a6e4052af20b858aa7df2df7d355b"},
+    {file = "pywin32-303-cp38-cp38-win_amd64.whl", hash = "sha256:793bf74fce164bcffd9d57bb13c2c15d56e43c9542a7b9687b4fccf8f8a41aba"},
+    {file = "pywin32-303-cp39-cp39-win32.whl", hash = "sha256:7d3271c98434617a11921c5ccf74615794d97b079e22ed7773790822735cc352"},
+    {file = "pywin32-303-cp39-cp39-win_amd64.whl", hash = "sha256:79cbb862c11b9af19bcb682891c1b91942ec2ff7de8151e2aea2e175899cda34"},
 ]
 pywinpty = [
-    {file = "pywinpty-1.1.6-cp310-none-win_amd64.whl", hash = "sha256:5f526f21b569b5610a61e3b6126259c76da979399598e5154498582df3736ade"},
-    {file = "pywinpty-1.1.6-cp36-none-win_amd64.whl", hash = "sha256:7576e14f42b31fa98b62d24ded79754d2ea4625570c016b38eb347ce158a30f2"},
-    {file = "pywinpty-1.1.6-cp37-none-win_amd64.whl", hash = "sha256:979ffdb9bdbe23db3f46fc7285fd6dbb86b80c12325a50582b211b3894072354"},
-    {file = "pywinpty-1.1.6-cp38-none-win_amd64.whl", hash = "sha256:2308b1fc77545427610a705799d4ead5e7f00874af3fb148a03e202437456a7e"},
-    {file = "pywinpty-1.1.6-cp39-none-win_amd64.whl", hash = "sha256:c703bf569a98ab7844b9daf37e88ab86f31862754ef6910a8b3824993a525c72"},
-    {file = "pywinpty-1.1.6.tar.gz", hash = "sha256:8808f07350c709119cc4464144d6e749637f98e15acc1e5d3c37db1953d2eebc"},
+    {file = "pywinpty-2.0.3-cp310-none-win_amd64.whl", hash = "sha256:7a330ef7a2ce284370b1a1fdd2a80c523585464fa5e5ab934c9f27220fa7feab"},
+    {file = "pywinpty-2.0.3-cp37-none-win_amd64.whl", hash = "sha256:6455f1075f978942d318f95616661c605d5e0f991c5b176c0c852d237aafefc0"},
+    {file = "pywinpty-2.0.3-cp38-none-win_amd64.whl", hash = "sha256:2e7a288a8121393c526d4e6ec7d65edef75d68c7787ab9560e438df867b75a5d"},
+    {file = "pywinpty-2.0.3-cp39-none-win_amd64.whl", hash = "sha256:def51627e6aa659f33ea7a0ea4c6b68365c83af4aad7940600f844746817a0ed"},
+    {file = "pywinpty-2.0.3.tar.gz", hash = "sha256:6b29a826e896105370c38d53904c3aaac6c36146a50448fc0ed5082cf9d092bc"},
 ]
 pyyaml = [
     {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
@@ -1649,20 +1667,20 @@ six = [
     {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
 ]
 terminado = [
-    {file = "terminado-0.12.1-py3-none-any.whl", hash = "sha256:09fdde344324a1c9c6e610ee4ca165c4bb7f5bbf982fceeeb38998a988ef8452"},
-    {file = "terminado-0.12.1.tar.gz", hash = "sha256:b20fd93cc57c1678c799799d117874367cc07a3d2d55be95205b1a88fa08393f"},
+    {file = "terminado-0.13.0-py3-none-any.whl", hash = "sha256:50a18654ad0cff153fdeb58711c9a7b25e0e2b74fb721dbaddd9e80d5612fac6"},
+    {file = "terminado-0.13.0.tar.gz", hash = "sha256:713531ccb5db7d4f544651f14050da79809030f00d1afa21462088cf32fb143a"},
 ]
 testpath = [
-    {file = "testpath-0.5.0-py3-none-any.whl", hash = "sha256:8044f9a0bab6567fc644a3593164e872543bb44225b0e24846e2c89237937589"},
-    {file = "testpath-0.5.0.tar.gz", hash = "sha256:1acf7a0bcd3004ae8357409fc33751e16d37ccc650921da1094a86581ad1e417"},
+    {file = "testpath-0.6.0-py3-none-any.whl", hash = "sha256:8ada9f80a2ac6fb0391aa7cdb1a7d11cfa8429f693eda83f74dde570fe6fa639"},
+    {file = "testpath-0.6.0.tar.gz", hash = "sha256:2f1b97e6442c02681ebe01bd84f531028a7caea1af3825000f52345c30285e0f"},
 ]
 toml = [
     {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
     {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
 ]
 tomli = [
-    {file = "tomli-1.2.2-py3-none-any.whl", hash = "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade"},
-    {file = "tomli-1.2.2.tar.gz", hash = "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee"},
+    {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"},
+    {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"},
 ]
 tornado = [
     {file = "tornado-6.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32"},
@@ -1712,48 +1730,42 @@ traitlets = [
     {file = "traitlets-4.3.3.tar.gz", hash = "sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7"},
 ]
 typed-ast = [
-    {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"},
-    {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"},
-    {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"},
-    {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"},
-    {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"},
-    {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"},
-    {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"},
-    {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"},
-    {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"},
-    {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"},
-    {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"},
-    {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"},
-    {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"},
-    {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"},
-    {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"},
-    {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"},
-    {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"},
-    {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"},
-    {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"},
-    {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"},
-    {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"},
-    {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"},
-    {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"},
-    {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"},
-    {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"},
-    {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"},
-    {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"},
-    {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"},
-    {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"},
-    {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"},
+    {file = "typed_ast-1.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:183b183b7771a508395d2cbffd6db67d6ad52958a5fdc99f450d954003900266"},
+    {file = "typed_ast-1.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:676d051b1da67a852c0447621fdd11c4e104827417bf216092ec3e286f7da596"},
+    {file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc2542e83ac8399752bc16e0b35e038bdb659ba237f4222616b4e83fb9654985"},
+    {file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74cac86cc586db8dfda0ce65d8bcd2bf17b58668dfcc3652762f3ef0e6677e76"},
+    {file = "typed_ast-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:18fe320f354d6f9ad3147859b6e16649a0781425268c4dde596093177660e71a"},
+    {file = "typed_ast-1.5.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:31d8c6b2df19a777bc8826770b872a45a1f30cfefcfd729491baa5237faae837"},
+    {file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:963a0ccc9a4188524e6e6d39b12c9ca24cc2d45a71cfdd04a26d883c922b4b78"},
+    {file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0eb77764ea470f14fcbb89d51bc6bbf5e7623446ac4ed06cbd9ca9495b62e36e"},
+    {file = "typed_ast-1.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:294a6903a4d087db805a7656989f613371915fc45c8cc0ddc5c5a0a8ad9bea4d"},
+    {file = "typed_ast-1.5.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:26a432dc219c6b6f38be20a958cbe1abffcc5492821d7e27f08606ef99e0dffd"},
+    {file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7407cfcad702f0b6c0e0f3e7ab876cd1d2c13b14ce770e412c0c4b9728a0f88"},
+    {file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f30ddd110634c2d7534b2d4e0e22967e88366b0d356b24de87419cc4410c41b7"},
+    {file = "typed_ast-1.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8c08d6625bb258179b6e512f55ad20f9dfef019bbfbe3095247401e053a3ea30"},
+    {file = "typed_ast-1.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:90904d889ab8e81a956f2c0935a523cc4e077c7847a836abee832f868d5c26a4"},
+    {file = "typed_ast-1.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bbebc31bf11762b63bf61aaae232becb41c5bf6b3461b80a4df7e791fabb3aca"},
+    {file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c29dd9a3a9d259c9fa19d19738d021632d673f6ed9b35a739f48e5f807f264fb"},
+    {file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:58ae097a325e9bb7a684572d20eb3e1809802c5c9ec7108e85da1eb6c1a3331b"},
+    {file = "typed_ast-1.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:da0a98d458010bf4fe535f2d1e367a2e2060e105978873c04c04212fb20543f7"},
+    {file = "typed_ast-1.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:33b4a19ddc9fc551ebabca9765d54d04600c4a50eda13893dadf67ed81d9a098"},
+    {file = "typed_ast-1.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1098df9a0592dd4c8c0ccfc2e98931278a6c6c53cb3a3e2cf7e9ee3b06153344"},
+    {file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c47c3b43fe3a39ddf8de1d40dbbfca60ac8530a36c9b198ea5b9efac75c09e"},
+    {file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f290617f74a610849bd8f5514e34ae3d09eafd521dceaa6cf68b3f4414266d4e"},
+    {file = "typed_ast-1.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:df05aa5b241e2e8045f5f4367a9f6187b09c4cdf8578bb219861c4e27c443db5"},
+    {file = "typed_ast-1.5.2.tar.gz", hash = "sha256:525a2d4088e70a9f75b08b3f87a51acc9cde640e19cc523c7e41aa355564ae27"},
 ]
 types-dataclasses = [
     {file = "types-dataclasses-0.6.4.tar.gz", hash = "sha256:2f7ab6c565cf05cc7f27f31a4c2fcc803384e319aab292807b857ddf1473429f"},
     {file = "types_dataclasses-0.6.4-py3-none-any.whl", hash = "sha256:fef6ed4742ca27996530c6d549cd704772a4a86e4781841c9bb387001e369ec3"},
 ]
 typing-extensions = [
-    {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"},
-    {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"},
+    {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"},
+    {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"},
 ]
 virtualenv = [
-    {file = "virtualenv-20.13.0-py2.py3-none-any.whl", hash = "sha256:339f16c4a86b44240ba7223d0f93a7887c3ca04b5f9c8129da7958447d079b09"},
-    {file = "virtualenv-20.13.0.tar.gz", hash = "sha256:d8458cf8d59d0ea495ad9b34c2599487f8a7772d796f9910858376d1600dd2dd"},
+    {file = "virtualenv-20.13.3-py2.py3-none-any.whl", hash = "sha256:dd448d1ded9f14d1a4bfa6bfc0c5b96ae3be3f2d6c6c159b23ddcfd701baa021"},
+    {file = "virtualenv-20.13.3.tar.gz", hash = "sha256:e9dd1a1359d70137559034c0f5433b34caf504af2dc756367be86a5a32967134"},
 ]
 wcwidth = [
     {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
diff --git a/pyproject.toml b/pyproject.toml
index e8ea9bf99c..2a6f3a89e7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -2,7 +2,7 @@
 name = "rich"
 homepage = "https://github.com/willmcgugan/rich"
 documentation = "https://rich.readthedocs.io/en/latest/"
-version = "11.1.0"
+version = "12.0.0"
 description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
 authors = ["Will McGugan "]
 license = "MIT"
@@ -31,7 +31,6 @@ typing-extensions = { version = ">=3.7.4, <5.0", python = "<3.8" }
 dataclasses = { version = ">=0.7,<0.9", python = "<3.7" }
 pygments = "^2.6.0"
 commonmark = "^0.9.0"
-colorama = "^0.4.0"
 ipywidgets = { version = "^7.5.1", optional = true }
 
 
@@ -39,13 +38,13 @@ ipywidgets = { version = "^7.5.1", optional = true }
 jupyter = ["ipywidgets"]
 
 [tool.poetry.dev-dependencies]
-pytest = "^6.2.5"
-black = "^21.11b1"
-mypy = "^0.930"
+pytest = "^7.0.0"
+black = "^22.1"
+mypy = "^0.931"
 pytest-cov = "^3.0.0"
 attrs = "^21.4.0"
 types-dataclasses = "^0.6.4"
-pre-commit = "^2.16.0"
+pre-commit = "^2.17.0"
 
 [build-system]
 requires = ["poetry-core>=1.0.0"]
diff --git a/rich/__init__.py b/rich/__init__.py
index ed11f5d7ea..01faa6e6b5 100644
--- a/rich/__init__.py
+++ b/rich/__init__.py
@@ -1,7 +1,7 @@
 """Rich text and beautiful formatting in the terminal."""
 
 import os
-from typing import Callable, IO, TYPE_CHECKING, Any, Optional
+from typing import Callable, IO, TYPE_CHECKING, Any, Optional, Union
 
 from ._extension import load_ipython_extension
 
@@ -73,7 +73,7 @@ def print_json(
     json: Optional[str] = None,
     *,
     data: Any = None,
-    indent: int = 2,
+    indent: Union[None, int, str] = 2,
     highlight: bool = True,
     skip_keys: bool = False,
     ensure_ascii: bool = True,
diff --git a/rich/__main__.py b/rich/__main__.py
index ea6ccfd9bd..7b3ffb4f2b 100644
--- a/rich/__main__.py
+++ b/rich/__main__.py
@@ -226,10 +226,7 @@ def iter_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]:
     console.print(test_card)
     taken = round((process_time() - start) * 1000.0, 1)
 
-    text = console.file.getvalue()
-    # https://bugs.python.org/issue37871
-    for line in text.splitlines(True):
-        print(line, end="")
+    Console().print(test_card)
 
     print(f"rendered in {pre_cache_taken}ms (cold cache)")
     print(f"rendered in {taken}ms (warm cache)")
diff --git a/rich/_inspect.py b/rich/_inspect.py
index b3652bfbdb..01713e5767 100644
--- a/rich/_inspect.py
+++ b/rich/_inspect.py
@@ -98,7 +98,8 @@ def _get_signature(self, name: str, obj: Any) -> Optional[Text]:
         source_filename: Optional[str] = None
         try:
             source_filename = getfile(obj)
-        except TypeError:
+        except (OSError, TypeError):
+            # OSError is raised if obj has no source file, e.g. when defined in REPL.
             pass
 
         callable_name = Text(name, style="inspect.callable")
diff --git a/rich/_spinners.py b/rich/_spinners.py
index dc1db0777e..d0bb1fe751 100644
--- a/rich/_spinners.py
+++ b/rich/_spinners.py
@@ -22,149 +22,36 @@
 SPINNERS = {
     "dots": {
         "interval": 80,
-        "frames": ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
+        "frames": "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏",
     },
-    "dots2": {"interval": 80, "frames": ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"]},
+    "dots2": {"interval": 80, "frames": "⣾⣽⣻⢿⡿⣟⣯⣷"},
     "dots3": {
         "interval": 80,
-        "frames": ["⠋", "⠙", "⠚", "⠞", "⠖", "⠦", "⠴", "⠲", "⠳", "⠓"],
+        "frames": "⠋⠙⠚⠞⠖⠦⠴⠲⠳⠓",
     },
     "dots4": {
         "interval": 80,
-        "frames": [
-            "⠄",
-            "⠆",
-            "⠇",
-            "⠋",
-            "⠙",
-            "⠸",
-            "⠰",
-            "⠠",
-            "⠰",
-            "⠸",
-            "⠙",
-            "⠋",
-            "⠇",
-            "⠆",
-        ],
+        "frames": "⠄⠆⠇⠋⠙⠸⠰⠠⠰⠸⠙⠋⠇⠆",
     },
     "dots5": {
         "interval": 80,
-        "frames": [
-            "⠋",
-            "⠙",
-            "⠚",
-            "⠒",
-            "⠂",
-            "⠂",
-            "⠒",
-            "⠲",
-            "⠴",
-            "⠦",
-            "⠖",
-            "⠒",
-            "⠐",
-            "⠐",
-            "⠒",
-            "⠓",
-            "⠋",
-        ],
+        "frames": "⠋⠙⠚⠒⠂⠂⠒⠲⠴⠦⠖⠒⠐⠐⠒⠓⠋",
     },
     "dots6": {
         "interval": 80,
-        "frames": [
-            "⠁",
-            "⠉",
-            "⠙",
-            "⠚",
-            "⠒",
-            "⠂",
-            "⠂",
-            "⠒",
-            "⠲",
-            "⠴",
-            "⠤",
-            "⠄",
-            "⠄",
-            "⠤",
-            "⠴",
-            "⠲",
-            "⠒",
-            "⠂",
-            "⠂",
-            "⠒",
-            "⠚",
-            "⠙",
-            "⠉",
-            "⠁",
-        ],
+        "frames": "⠁⠉⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄⠄⠤⠴⠲⠒⠂⠂⠒⠚⠙⠉⠁",
     },
     "dots7": {
         "interval": 80,
-        "frames": [
-            "⠈",
-            "⠉",
-            "⠋",
-            "⠓",
-            "⠒",
-            "⠐",
-            "⠐",
-            "⠒",
-            "⠖",
-            "⠦",
-            "⠤",
-            "⠠",
-            "⠠",
-            "⠤",
-            "⠦",
-            "⠖",
-            "⠒",
-            "⠐",
-            "⠐",
-            "⠒",
-            "⠓",
-            "⠋",
-            "⠉",
-            "⠈",
-        ],
+        "frames": "⠈⠉⠋⠓⠒⠐⠐⠒⠖⠦⠤⠠⠠⠤⠦⠖⠒⠐⠐⠒⠓⠋⠉⠈",
     },
     "dots8": {
         "interval": 80,
-        "frames": [
-            "⠁",
-            "⠁",
-            "⠉",
-            "⠙",
-            "⠚",
-            "⠒",
-            "⠂",
-            "⠂",
-            "⠒",
-            "⠲",
-            "⠴",
-            "⠤",
-            "⠄",
-            "⠄",
-            "⠤",
-            "⠠",
-            "⠠",
-            "⠤",
-            "⠦",
-            "⠖",
-            "⠒",
-            "⠐",
-            "⠐",
-            "⠒",
-            "⠓",
-            "⠋",
-            "⠉",
-            "⠈",
-            "⠈",
-        ],
+        "frames": "⠁⠁⠉⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄⠄⠤⠠⠠⠤⠦⠖⠒⠐⠐⠒⠓⠋⠉⠈⠈",
     },
-    "dots9": {"interval": 80, "frames": ["⢹", "⢺", "⢼", "⣸", "⣇", "⡧", "⡗", "⡏"]},
-    "dots10": {"interval": 80, "frames": ["⢄", "⢂", "⢁", "⡁", "⡈", "⡐", "⡠"]},
-    "dots11": {"interval": 100, "frames": ["⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐", "⠈"]},
+    "dots9": {"interval": 80, "frames": "⢹⢺⢼⣸⣇⡧⡗⡏"},
+    "dots10": {"interval": 80, "frames": "⢄⢂⢁⡁⡈⡐⡠"},
+    "dots11": {"interval": 100, "frames": "⠁⠂⠄⡀⢀⠠⠐⠈"},
     "dots12": {
         "interval": 80,
         "frames": [
@@ -228,315 +115,62 @@
     },
     "dots8Bit": {
         "interval": 80,
-        "frames": [
-            "⠀",
-            "⠁",
-            "⠂",
-            "⠃",
-            "⠄",
-            "⠅",
-            "⠆",
-            "⠇",
-            "⡀",
-            "⡁",
-            "⡂",
-            "⡃",
-            "⡄",
-            "⡅",
-            "⡆",
-            "⡇",
-            "⠈",
-            "⠉",
-            "⠊",
-            "⠋",
-            "⠌",
-            "⠍",
-            "⠎",
-            "⠏",
-            "⡈",
-            "⡉",
-            "⡊",
-            "⡋",
-            "⡌",
-            "⡍",
-            "⡎",
-            "⡏",
-            "⠐",
-            "⠑",
-            "⠒",
-            "⠓",
-            "⠔",
-            "⠕",
-            "⠖",
-            "⠗",
-            "⡐",
-            "⡑",
-            "⡒",
-            "⡓",
-            "⡔",
-            "⡕",
-            "⡖",
-            "⡗",
-            "⠘",
-            "⠙",
-            "⠚",
-            "⠛",
-            "⠜",
-            "⠝",
-            "⠞",
-            "⠟",
-            "⡘",
-            "⡙",
-            "⡚",
-            "⡛",
-            "⡜",
-            "⡝",
-            "⡞",
-            "⡟",
-            "⠠",
-            "⠡",
-            "⠢",
-            "⠣",
-            "⠤",
-            "⠥",
-            "⠦",
-            "⠧",
-            "⡠",
-            "⡡",
-            "⡢",
-            "⡣",
-            "⡤",
-            "⡥",
-            "⡦",
-            "⡧",
-            "⠨",
-            "⠩",
-            "⠪",
-            "⠫",
-            "⠬",
-            "⠭",
-            "⠮",
-            "⠯",
-            "⡨",
-            "⡩",
-            "⡪",
-            "⡫",
-            "⡬",
-            "⡭",
-            "⡮",
-            "⡯",
-            "⠰",
-            "⠱",
-            "⠲",
-            "⠳",
-            "⠴",
-            "⠵",
-            "⠶",
-            "⠷",
-            "⡰",
-            "⡱",
-            "⡲",
-            "⡳",
-            "⡴",
-            "⡵",
-            "⡶",
-            "⡷",
-            "⠸",
-            "⠹",
-            "⠺",
-            "⠻",
-            "⠼",
-            "⠽",
-            "⠾",
-            "⠿",
-            "⡸",
-            "⡹",
-            "⡺",
-            "⡻",
-            "⡼",
-            "⡽",
-            "⡾",
-            "⡿",
-            "⢀",
-            "⢁",
-            "⢂",
-            "⢃",
-            "⢄",
-            "⢅",
-            "⢆",
-            "⢇",
-            "⣀",
-            "⣁",
-            "⣂",
-            "⣃",
-            "⣄",
-            "⣅",
-            "⣆",
-            "⣇",
-            "⢈",
-            "⢉",
-            "⢊",
-            "⢋",
-            "⢌",
-            "⢍",
-            "⢎",
-            "⢏",
-            "⣈",
-            "⣉",
-            "⣊",
-            "⣋",
-            "⣌",
-            "⣍",
-            "⣎",
-            "⣏",
-            "⢐",
-            "⢑",
-            "⢒",
-            "⢓",
-            "⢔",
-            "⢕",
-            "⢖",
-            "⢗",
-            "⣐",
-            "⣑",
-            "⣒",
-            "⣓",
-            "⣔",
-            "⣕",
-            "⣖",
-            "⣗",
-            "⢘",
-            "⢙",
-            "⢚",
-            "⢛",
-            "⢜",
-            "⢝",
-            "⢞",
-            "⢟",
-            "⣘",
-            "⣙",
-            "⣚",
-            "⣛",
-            "⣜",
-            "⣝",
-            "⣞",
-            "⣟",
-            "⢠",
-            "⢡",
-            "⢢",
-            "⢣",
-            "⢤",
-            "⢥",
-            "⢦",
-            "⢧",
-            "⣠",
-            "⣡",
-            "⣢",
-            "⣣",
-            "⣤",
-            "⣥",
-            "⣦",
-            "⣧",
-            "⢨",
-            "⢩",
-            "⢪",
-            "⢫",
-            "⢬",
-            "⢭",
-            "⢮",
-            "⢯",
-            "⣨",
-            "⣩",
-            "⣪",
-            "⣫",
-            "⣬",
-            "⣭",
-            "⣮",
-            "⣯",
-            "⢰",
-            "⢱",
-            "⢲",
-            "⢳",
-            "⢴",
-            "⢵",
-            "⢶",
-            "⢷",
-            "⣰",
-            "⣱",
-            "⣲",
-            "⣳",
-            "⣴",
-            "⣵",
-            "⣶",
-            "⣷",
-            "⢸",
-            "⢹",
-            "⢺",
-            "⢻",
-            "⢼",
-            "⢽",
-            "⢾",
-            "⢿",
-            "⣸",
-            "⣹",
-            "⣺",
-            "⣻",
-            "⣼",
-            "⣽",
-            "⣾",
-            "⣿",
-        ],
+        "frames": "⠀⠁⠂⠃⠄⠅⠆⠇⡀⡁⡂⡃⡄⡅⡆⡇⠈⠉⠊⠋⠌⠍⠎⠏⡈⡉⡊⡋⡌⡍⡎⡏⠐⠑⠒⠓⠔⠕⠖⠗⡐⡑⡒⡓⡔⡕⡖⡗⠘⠙⠚⠛⠜⠝⠞⠟⡘⡙"
+        "⡚⡛⡜⡝⡞⡟⠠⠡⠢⠣⠤⠥⠦⠧⡠⡡⡢⡣⡤⡥⡦⡧⠨⠩⠪⠫⠬⠭⠮⠯⡨⡩⡪⡫⡬⡭⡮⡯⠰⠱⠲⠳⠴⠵⠶⠷⡰⡱⡲⡳⡴⡵⡶⡷⠸⠹⠺⠻"
+        "⠼⠽⠾⠿⡸⡹⡺⡻⡼⡽⡾⡿⢀⢁⢂⢃⢄⢅⢆⢇⣀⣁⣂⣃⣄⣅⣆⣇⢈⢉⢊⢋⢌⢍⢎⢏⣈⣉⣊⣋⣌⣍⣎⣏⢐⢑⢒⢓⢔⢕⢖⢗⣐⣑⣒⣓⣔⣕"
+        "⣖⣗⢘⢙⢚⢛⢜⢝⢞⢟⣘⣙⣚⣛⣜⣝⣞⣟⢠⢡⢢⢣⢤⢥⢦⢧⣠⣡⣢⣣⣤⣥⣦⣧⢨⢩⢪⢫⢬⢭⢮⢯⣨⣩⣪⣫⣬⣭⣮⣯⢰⢱⢲⢳⢴⢵⢶⢷"
+        "⣰⣱⣲⣳⣴⣵⣶⣷⢸⢹⢺⢻⢼⢽⢾⢿⣸⣹⣺⣻⣼⣽⣾⣿",
     },
     "line": {"interval": 130, "frames": ["-", "\\", "|", "/"]},
-    "line2": {"interval": 100, "frames": ["⠂", "-", "–", "—", "–", "-"]},
-    "pipe": {"interval": 100, "frames": ["┤", "┘", "┴", "└", "├", "┌", "┬", "┐"]},
+    "line2": {"interval": 100, "frames": "⠂-–—–-"},
+    "pipe": {"interval": 100, "frames": "┤┘┴└├┌┬┐"},
     "simpleDots": {"interval": 400, "frames": [".  ", ".. ", "...", "   "]},
     "simpleDotsScrolling": {
         "interval": 200,
         "frames": [".  ", ".. ", "...", " ..", "  .", "   "],
     },
-    "star": {"interval": 70, "frames": ["✶", "✸", "✹", "✺", "✹", "✷"]},
-    "star2": {"interval": 80, "frames": ["+", "x", "*"]},
+    "star": {"interval": 70, "frames": "✶✸✹✺✹✷"},
+    "star2": {"interval": 80, "frames": "+x*"},
     "flip": {
         "interval": 70,
-        "frames": ["_", "_", "_", "-", "`", "`", "'", "´", "-", "_", "_", "_"],
+        "frames": "___-``'´-___",
     },
-    "hamburger": {"interval": 100, "frames": ["☱", "☲", "☴"]},
+    "hamburger": {"interval": 100, "frames": "☱☲☴"},
     "growVertical": {
         "interval": 120,
-        "frames": ["▁", "▃", "▄", "▅", "▆", "▇", "▆", "▅", "▄", "▃"],
+        "frames": "▁▃▄▅▆▇▆▅▄▃",
     },
     "growHorizontal": {
         "interval": 120,
-        "frames": ["▏", "▎", "▍", "▌", "▋", "▊", "▉", "▊", "▋", "▌", "▍", "▎"],
+        "frames": "▏▎▍▌▋▊▉▊▋▌▍▎",
     },
-    "balloon": {"interval": 140, "frames": [" ", ".", "o", "O", "@", "*", " "]},
-    "balloon2": {"interval": 120, "frames": [".", "o", "O", "°", "O", "o", "."]},
-    "noise": {"interval": 100, "frames": ["▓", "▒", "░"]},
-    "bounce": {"interval": 120, "frames": ["⠁", "⠂", "⠄", "⠂"]},
-    "boxBounce": {"interval": 120, "frames": ["▖", "▘", "▝", "▗"]},
-    "boxBounce2": {"interval": 100, "frames": ["▌", "▀", "▐", "▄"]},
-    "triangle": {"interval": 50, "frames": ["◢", "◣", "◤", "◥"]},
-    "arc": {"interval": 100, "frames": ["◜", "◠", "◝", "◞", "◡", "◟"]},
-    "circle": {"interval": 120, "frames": ["◡", "⊙", "◠"]},
-    "squareCorners": {"interval": 180, "frames": ["◰", "◳", "◲", "◱"]},
-    "circleQuarters": {"interval": 120, "frames": ["◴", "◷", "◶", "◵"]},
-    "circleHalves": {"interval": 50, "frames": ["◐", "◓", "◑", "◒"]},
-    "squish": {"interval": 100, "frames": ["╫", "╪"]},
-    "toggle": {"interval": 250, "frames": ["⊶", "⊷"]},
-    "toggle2": {"interval": 80, "frames": ["▫", "▪"]},
-    "toggle3": {"interval": 120, "frames": ["□", "■"]},
-    "toggle4": {"interval": 100, "frames": ["■", "□", "▪", "▫"]},
-    "toggle5": {"interval": 100, "frames": ["▮", "▯"]},
-    "toggle6": {"interval": 300, "frames": ["ဝ", "၀"]},
-    "toggle7": {"interval": 80, "frames": ["⦾", "⦿"]},
-    "toggle8": {"interval": 100, "frames": ["◍", "◌"]},
-    "toggle9": {"interval": 100, "frames": ["◉", "◎"]},
-    "toggle10": {"interval": 100, "frames": ["㊂", "㊀", "㊁"]},
-    "toggle11": {"interval": 50, "frames": ["⧇", "⧆"]},
-    "toggle12": {"interval": 120, "frames": ["☗", "☖"]},
-    "toggle13": {"interval": 80, "frames": ["=", "*", "-"]},
-    "arrow": {"interval": 100, "frames": ["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"]},
+    "balloon": {"interval": 140, "frames": " .oO@* "},
+    "balloon2": {"interval": 120, "frames": ".oO°Oo."},
+    "noise": {"interval": 100, "frames": "▓▒░"},
+    "bounce": {"interval": 120, "frames": "⠁⠂⠄⠂"},
+    "boxBounce": {"interval": 120, "frames": "▖▘▝▗"},
+    "boxBounce2": {"interval": 100, "frames": "▌▀▐▄"},
+    "triangle": {"interval": 50, "frames": "◢◣◤◥"},
+    "arc": {"interval": 100, "frames": "◜◠◝◞◡◟"},
+    "circle": {"interval": 120, "frames": "◡⊙◠"},
+    "squareCorners": {"interval": 180, "frames": "◰◳◲◱"},
+    "circleQuarters": {"interval": 120, "frames": "◴◷◶◵"},
+    "circleHalves": {"interval": 50, "frames": "◐◓◑◒"},
+    "squish": {"interval": 100, "frames": "╫╪"},
+    "toggle": {"interval": 250, "frames": "⊶⊷"},
+    "toggle2": {"interval": 80, "frames": "▫▪"},
+    "toggle3": {"interval": 120, "frames": "□■"},
+    "toggle4": {"interval": 100, "frames": "■□▪▫"},
+    "toggle5": {"interval": 100, "frames": "▮▯"},
+    "toggle6": {"interval": 300, "frames": "ဝ၀"},
+    "toggle7": {"interval": 80, "frames": "⦾⦿"},
+    "toggle8": {"interval": 100, "frames": "◍◌"},
+    "toggle9": {"interval": 100, "frames": "◉◎"},
+    "toggle10": {"interval": 100, "frames": "㊂㊀㊁"},
+    "toggle11": {"interval": 50, "frames": "⧇⧆"},
+    "toggle12": {"interval": 120, "frames": "☗☖"},
+    "toggle13": {"interval": 80, "frames": "=*-"},
+    "arrow": {"interval": 100, "frames": "←↖↑↗→↘↓↙"},
     "arrow2": {
         "interval": 80,
         "frames": ["⬆️ ", "↗️ ", "➡️ ", "↘️ ", "⬇️ ", "↙️ ", "⬅️ ", "↖️ "],
@@ -769,7 +403,7 @@
             "▐/|____________▌",
         ],
     },
-    "dqpb": {"interval": 100, "frames": ["d", "q", "p", "b"]},
+    "dqpb": {"interval": 100, "frames": "dqpb"},
     "weather": {
         "interval": 100,
         "frames": [
@@ -798,7 +432,7 @@
             "☀️ ",
         ],
     },
-    "christmas": {"interval": 400, "frames": ["🌲", "🎄"]},
+    "christmas": {"interval": 400, "frames": "🌲🎄"},
     "grenade": {
         "interval": 80,
         "frames": [
@@ -819,7 +453,7 @@
         ],
     },
     "point": {"interval": 125, "frames": ["∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙"]},
-    "layer": {"interval": 150, "frames": ["-", "=", "≡"]},
+    "layer": {"interval": 150, "frames": "-=≡"},
     "betaWave": {
         "interval": 80,
         "frames": [
diff --git a/rich/_win32_console.py b/rich/_win32_console.py
new file mode 100644
index 0000000000..c88287564a
--- /dev/null
+++ b/rich/_win32_console.py
@@ -0,0 +1,658 @@
+"""Light wrapper around the Win32 Console API - this module should only be imported on Windows
+
+The API that this module wraps is documented at https://docs.microsoft.com/en-us/windows/console/console-functions
+"""
+import ctypes
+import sys
+from typing import IO, Any, NamedTuple, Type, cast
+
+windll: Any = None
+if sys.platform == "win32":
+    windll = ctypes.LibraryLoader(ctypes.WinDLL)
+else:
+    raise ImportError(f"{__name__} can only be imported on Windows")
+
+import time
+from ctypes import Structure, byref, wintypes
+
+from rich.color import ColorSystem
+from rich.style import Style
+
+STDOUT = -11
+ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4
+
+COORD = wintypes._COORD
+
+
+class LegacyWindowsError(Exception):
+    pass
+
+
+class WindowsCoordinates(NamedTuple):
+    """Coordinates in the Windows Console API are (y, x), not (x, y).
+    This class is intended to prevent that confusion.
+    Rows and columns are indexed from 0.
+    This class can be used in place of wintypes._COORD in arguments and argtypes.
+    """
+
+    row: int
+    col: int
+
+    @classmethod
+    def from_param(cls, value: "WindowsCoordinates") -> COORD:
+        """Converts a WindowsCoordinates into a wintypes _COORD structure.
+        This classmethod is internally called by ctypes to perform the conversion.
+
+        Args:
+            value (WindowsCoordinates): The input coordinates to convert.
+
+        Returns:
+            wintypes._COORD: The converted coordinates struct.
+        """
+        return COORD(value.col, value.row)
+
+
+class CONSOLE_SCREEN_BUFFER_INFO(Structure):
+    _fields_ = [
+        ("dwSize", COORD),
+        ("dwCursorPosition", COORD),
+        ("wAttributes", wintypes.WORD),
+        ("srWindow", wintypes.SMALL_RECT),
+        ("dwMaximumWindowSize", COORD),
+    ]
+
+
+class CONSOLE_CURSOR_INFO(ctypes.Structure):
+    _fields_ = [("dwSize", wintypes.DWORD), ("bVisible", wintypes.BOOL)]
+
+
+_GetStdHandle = windll.kernel32.GetStdHandle
+_GetStdHandle.argtypes = [
+    wintypes.DWORD,
+]
+_GetStdHandle.restype = wintypes.HANDLE
+
+
+def GetStdHandle(handle: int = STDOUT) -> wintypes.HANDLE:
+    """Retrieves a handle to the specified standard device (standard input, standard output, or standard error).
+
+    Args:
+        handle (int): Integer identifier for the handle. Defaults to -11 (stdout).
+
+    Returns:
+        wintypes.HANDLE: The handle
+    """
+    return cast(wintypes.HANDLE, _GetStdHandle(handle))
+
+
+_GetConsoleMode = windll.kernel32.GetConsoleMode
+_GetConsoleMode.argtypes = [wintypes.HANDLE, wintypes.LPDWORD]
+_GetConsoleMode.restype = wintypes.BOOL
+
+
+def GetConsoleMode(std_handle: wintypes.HANDLE) -> int:
+    """Retrieves the current input mode of a console's input buffer
+    or the current output mode of a console screen buffer.
+
+    Args:
+        std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
+
+    Raises:
+        LegacyWindowsError: If any error occurs while calling the Windows console API.
+
+    Returns:
+        int: Value representing the current console mode as documented at
+            https://docs.microsoft.com/en-us/windows/console/getconsolemode#parameters
+    """
+
+    console_mode = wintypes.DWORD()
+    success = bool(_GetConsoleMode(std_handle, console_mode))
+    if not success:
+        raise LegacyWindowsError("Unable to get legacy Windows Console Mode")
+    return console_mode.value
+
+
+_FillConsoleOutputCharacterW = windll.kernel32.FillConsoleOutputCharacterW
+_FillConsoleOutputCharacterW.argtypes = [
+    wintypes.HANDLE,
+    ctypes.c_char,
+    wintypes.DWORD,
+    cast(Type[COORD], WindowsCoordinates),
+    ctypes.POINTER(wintypes.DWORD),
+]
+_FillConsoleOutputCharacterW.restype = wintypes.BOOL
+
+
+def FillConsoleOutputCharacter(
+    std_handle: wintypes.HANDLE,
+    char: str,
+    length: int,
+    start: WindowsCoordinates,
+) -> int:
+    """Writes a character to the console screen buffer a specified number of times, beginning at the specified coordinates.
+
+    Args:
+        std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
+        char (str): The character to write. Must be a string of length 1.
+        length (int): The number of times to write the character.
+        start (WindowsCoordinates): The coordinates to start writing at.
+
+    Returns:
+        int: The number of characters written.
+    """
+    character = ctypes.c_char(char.encode())
+    num_characters = wintypes.DWORD(length)
+    num_written = wintypes.DWORD(0)
+    _FillConsoleOutputCharacterW(
+        std_handle,
+        character,
+        num_characters,
+        start,
+        byref(num_written),
+    )
+    return num_written.value
+
+
+_FillConsoleOutputAttribute = windll.kernel32.FillConsoleOutputAttribute
+_FillConsoleOutputAttribute.argtypes = [
+    wintypes.HANDLE,
+    wintypes.WORD,
+    wintypes.DWORD,
+    cast(Type[COORD], WindowsCoordinates),
+    ctypes.POINTER(wintypes.DWORD),
+]
+_FillConsoleOutputAttribute.restype = wintypes.BOOL
+
+
+def FillConsoleOutputAttribute(
+    std_handle: wintypes.HANDLE,
+    attributes: int,
+    length: int,
+    start: WindowsCoordinates,
+) -> int:
+    """Sets the character attributes for a specified number of character cells,
+    beginning at the specified coordinates in a screen buffer.
+
+    Args:
+        std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
+        attributes (int): Integer value representing the foreground and background colours of the cells.
+        length (int): The number of cells to set the output attribute of.
+        start (WindowsCoordinates): The coordinates of the first cell whose attributes are to be set.
+
+    Returns:
+        int: The number of cells whose attributes were actually set.
+    """
+    num_cells = wintypes.DWORD(length)
+    style_attrs = wintypes.WORD(attributes)
+    num_written = wintypes.DWORD(0)
+    _FillConsoleOutputAttribute(
+        std_handle, style_attrs, num_cells, start, byref(num_written)
+    )
+    return num_written.value
+
+
+_SetConsoleTextAttribute = windll.kernel32.SetConsoleTextAttribute
+_SetConsoleTextAttribute.argtypes = [
+    wintypes.HANDLE,
+    wintypes.WORD,
+]
+_SetConsoleTextAttribute.restype = wintypes.BOOL
+
+
+def SetConsoleTextAttribute(
+    std_handle: wintypes.HANDLE, attributes: wintypes.WORD
+) -> bool:
+    """Set the colour attributes for all text written after this function is called.
+
+    Args:
+        std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
+        attributes (int): Integer value representing the foreground and background colours.
+
+
+    Returns:
+        bool: True if the attribute was set successfully, otherwise False.
+    """
+    return bool(_SetConsoleTextAttribute(std_handle, attributes))
+
+
+_GetConsoleScreenBufferInfo = windll.kernel32.GetConsoleScreenBufferInfo
+_GetConsoleScreenBufferInfo.argtypes = [
+    wintypes.HANDLE,
+    ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO),
+]
+_GetConsoleScreenBufferInfo.restype = wintypes.BOOL
+
+
+def GetConsoleScreenBufferInfo(
+    std_handle: wintypes.HANDLE,
+) -> CONSOLE_SCREEN_BUFFER_INFO:
+    """Retrieves information about the specified console screen buffer.
+
+    Args:
+        std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
+
+    Returns:
+        CONSOLE_SCREEN_BUFFER_INFO: A CONSOLE_SCREEN_BUFFER_INFO ctype struct contain information about
+            screen size, cursor position, colour attributes, and more."""
+    console_screen_buffer_info = CONSOLE_SCREEN_BUFFER_INFO()
+    _GetConsoleScreenBufferInfo(std_handle, byref(console_screen_buffer_info))
+    return console_screen_buffer_info
+
+
+_SetConsoleCursorPosition = windll.kernel32.SetConsoleCursorPosition
+_SetConsoleCursorPosition.argtypes = [
+    wintypes.HANDLE,
+    cast(Type[COORD], WindowsCoordinates),
+]
+_SetConsoleCursorPosition.restype = wintypes.BOOL
+
+
+def SetConsoleCursorPosition(
+    std_handle: wintypes.HANDLE, coords: WindowsCoordinates
+) -> bool:
+    """Set the position of the cursor in the console screen
+
+    Args:
+        std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
+        coords (WindowsCoordinates): The coordinates to move the cursor to.
+
+    Returns:
+        bool: True if the function succeeds, otherwise False.
+    """
+    return bool(_SetConsoleCursorPosition(std_handle, coords))
+
+
+_SetConsoleCursorInfo = windll.kernel32.SetConsoleCursorInfo
+_SetConsoleCursorInfo.argtypes = [
+    wintypes.HANDLE,
+    ctypes.POINTER(CONSOLE_CURSOR_INFO),
+]
+_SetConsoleCursorInfo.restype = wintypes.BOOL
+
+
+def SetConsoleCursorInfo(
+    std_handle: wintypes.HANDLE, cursor_info: CONSOLE_CURSOR_INFO
+) -> bool:
+    """Set the cursor info - used for adjusting cursor visibility and width
+
+    Args:
+        std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
+        cursor_info (CONSOLE_CURSOR_INFO): CONSOLE_CURSOR_INFO ctype struct containing the new cursor info.
+
+    Returns:
+          bool: True if the function succeeds, otherwise False.
+    """
+    return bool(_SetConsoleCursorInfo(std_handle, byref(cursor_info)))
+
+
+_SetConsoleTitle = windll.kernel32.SetConsoleTitleW
+_SetConsoleTitle.argtypes = [wintypes.LPCWSTR]
+_SetConsoleTitle.restype = wintypes.BOOL
+
+
+def SetConsoleTitle(title: str) -> bool:
+    """Sets the title of the current console window
+
+    Args:
+        title (str): The new title of the console window.
+
+    Returns:
+        bool: True if the function succeeds, otherwise False.
+    """
+    return bool(_SetConsoleTitle(title))
+
+
+_WriteConsole = windll.kernel32.WriteConsoleW
+_WriteConsole.argtypes = [
+    wintypes.HANDLE,
+    wintypes.LPWSTR,
+    wintypes.DWORD,
+    wintypes.LPDWORD,
+    wintypes.LPVOID,
+]
+_WriteConsole.restype = wintypes.BOOL
+
+
+def WriteConsole(std_handle: wintypes.HANDLE, text: str) -> bool:
+    """Write a string of text to the console, starting at the current cursor position
+
+    Args:
+        std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
+        text (str): The text to write.
+
+    Returns:
+        bool: True if the function succeeds, otherwise False.
+    """
+    buffer = wintypes.LPWSTR(text)
+    num_chars_written = wintypes.LPDWORD()
+    return bool(
+        _WriteConsole(
+            std_handle,
+            buffer,
+            wintypes.DWORD(len(text)),
+            num_chars_written,
+            wintypes.LPVOID(None),
+        )
+    )
+
+
+class LegacyWindowsTerm:
+    """This class allows interaction with the legacy Windows Console API. It should only be used in the context
+    of environments where virtual terminal processing is not available. However, if it is used in a Windows environment,
+    the entire API should work.
+
+    Args:
+        file (IO[str]): The file which the Windows Console API HANDLE is retrieved from, defaults to sys.stdout.
+    """
+
+    BRIGHT_BIT = 8
+
+    # Indices are ANSI color numbers, values are the corresponding Windows Console API color numbers
+    ANSI_TO_WINDOWS = [
+        0,  # black                      The Windows colours are defined in wincon.h as follows:
+        4,  # red                         define FOREGROUND_BLUE            0x0001 -- 0000 0001
+        2,  # green                       define FOREGROUND_GREEN           0x0002 -- 0000 0010
+        6,  # yellow                      define FOREGROUND_RED             0x0004 -- 0000 0100
+        1,  # blue                        define FOREGROUND_INTENSITY       0x0008 -- 0000 1000
+        5,  # magenta                     define BACKGROUND_BLUE            0x0010 -- 0001 0000
+        3,  # cyan                        define BACKGROUND_GREEN           0x0020 -- 0010 0000
+        7,  # white                       define BACKGROUND_RED             0x0040 -- 0100 0000
+        8,  # bright black (grey)         define BACKGROUND_INTENSITY       0x0080 -- 1000 0000
+        12,  # bright red
+        10,  # bright green
+        14,  # bright yellow
+        9,  # bright blue
+        13,  # bright magenta
+        11,  # bright cyan
+        15,  # bright white
+    ]
+
+    def __init__(self) -> None:
+        handle = GetStdHandle(STDOUT)
+        self._handle = handle
+        default_text = GetConsoleScreenBufferInfo(handle).wAttributes
+        self._default_text = default_text
+
+        self._default_fore = default_text & 7
+        self._default_back = (default_text >> 4) & 7
+        self._default_attrs = self._default_fore | (self._default_back << 4)
+
+    @property
+    def cursor_position(self) -> WindowsCoordinates:
+        """Returns the current position of the cursor (0-based)
+
+        Returns:
+            WindowsCoordinates: The current cursor position.
+        """
+        coord: COORD = GetConsoleScreenBufferInfo(self._handle).dwCursorPosition
+        return WindowsCoordinates(row=cast(int, coord.Y), col=cast(int, coord.X))
+
+    @property
+    def screen_size(self) -> WindowsCoordinates:
+        """Returns the current size of the console screen buffer, in character columns and rows
+
+        Returns:
+            WindowsCoordinates: The width and height of the screen as WindowsCoordinates.
+        """
+        screen_size: COORD = GetConsoleScreenBufferInfo(self._handle).dwSize
+        return WindowsCoordinates(
+            row=cast(int, screen_size.Y), col=cast(int, screen_size.X)
+        )
+
+    def write_text(self, text: str) -> None:
+        """Write text directly to the terminal without any modification of styles
+
+        Args:
+            text (str): The text to write to the console
+        """
+        WriteConsole(self._handle, text)
+
+    def write_styled(self, text: str, style: Style) -> None:
+        """Write styled text to the terminal.
+
+        Args:
+            text (str): The text to write
+            style (Style): The style of the text
+        """
+        color = style.color
+        bgcolor = style.bgcolor
+        if style.reverse:
+            color, bgcolor = bgcolor, color
+
+        if color:
+            fore = color.downgrade(ColorSystem.WINDOWS).number
+            fore = fore if fore is not None else 7  # Default to ANSI 7: White
+            if style.bold:
+                fore = fore | self.BRIGHT_BIT
+            if style.dim:
+                fore = fore & ~self.BRIGHT_BIT
+            fore = self.ANSI_TO_WINDOWS[fore]
+        else:
+            fore = self._default_fore
+
+        if bgcolor:
+            back = bgcolor.downgrade(ColorSystem.WINDOWS).number
+            back = back if back is not None else 0  # Default to ANSI 0: Black
+            back = self.ANSI_TO_WINDOWS[back]
+        else:
+            back = self._default_back
+
+        assert fore is not None
+        assert back is not None
+
+        SetConsoleTextAttribute(
+            self._handle, attributes=ctypes.c_ushort(fore | (back << 4))
+        )
+        self.write_text(text)
+        SetConsoleTextAttribute(self._handle, attributes=self._default_text)
+
+    def move_cursor_to(self, new_position: WindowsCoordinates) -> None:
+        """Set the position of the cursor
+
+        Args:
+            new_position (WindowsCoordinates): The WindowsCoordinates representing the new position of the cursor.
+        """
+        if new_position.col < 0 or new_position.row < 0:
+            return
+        SetConsoleCursorPosition(self._handle, coords=new_position)
+
+    def erase_line(self) -> None:
+        """Erase all content on the line the cursor is currently located at"""
+        screen_size = self.screen_size
+        cursor_position = self.cursor_position
+        cells_to_erase = screen_size.col
+        start_coordinates = WindowsCoordinates(row=cursor_position.row, col=0)
+        FillConsoleOutputCharacter(
+            self._handle, " ", length=cells_to_erase, start=start_coordinates
+        )
+        FillConsoleOutputAttribute(
+            self._handle,
+            self._default_attrs,
+            length=cells_to_erase,
+            start=start_coordinates,
+        )
+
+    def erase_end_of_line(self) -> None:
+        """Erase all content from the cursor position to the end of that line"""
+        cursor_position = self.cursor_position
+        cells_to_erase = self.screen_size.col - cursor_position.col
+        FillConsoleOutputCharacter(
+            self._handle, " ", length=cells_to_erase, start=cursor_position
+        )
+        FillConsoleOutputAttribute(
+            self._handle,
+            self._default_attrs,
+            length=cells_to_erase,
+            start=cursor_position,
+        )
+
+    def erase_start_of_line(self) -> None:
+        """Erase all content from the cursor position to the start of that line"""
+        row, col = self.cursor_position
+        start = WindowsCoordinates(row, 0)
+        FillConsoleOutputCharacter(self._handle, " ", length=col, start=start)
+        FillConsoleOutputAttribute(
+            self._handle, self._default_attrs, length=col, start=start
+        )
+
+    def move_cursor_up(self) -> None:
+        """Move the cursor up a single cell"""
+        cursor_position = self.cursor_position
+        SetConsoleCursorPosition(
+            self._handle,
+            coords=WindowsCoordinates(
+                row=cursor_position.row - 1, col=cursor_position.col
+            ),
+        )
+
+    def move_cursor_down(self) -> None:
+        """Move the cursor down a single cell"""
+        cursor_position = self.cursor_position
+        SetConsoleCursorPosition(
+            self._handle,
+            coords=WindowsCoordinates(
+                row=cursor_position.row + 1,
+                col=cursor_position.col,
+            ),
+        )
+
+    def move_cursor_forward(self) -> None:
+        """Move the cursor forward a single cell. Wrap to the next line if required."""
+        row, col = self.cursor_position
+        if col == self.screen_size.col - 1:
+            row += 1
+            col = 0
+        else:
+            col += 1
+        SetConsoleCursorPosition(
+            self._handle, coords=WindowsCoordinates(row=row, col=col)
+        )
+
+    def move_cursor_to_column(self, column: int) -> None:
+        """Move cursor to the column specified by the zero-based column index, staying on the same row
+
+        Args:
+            column (int): The zero-based column index to move the cursor to.
+        """
+        row, _ = self.cursor_position
+        SetConsoleCursorPosition(self._handle, coords=WindowsCoordinates(row, column))
+
+    def move_cursor_backward(self) -> None:
+        """Move the cursor backward a single cell. Wrap to the previous line if required."""
+        row, col = self.cursor_position
+        if col == 0:
+            row -= 1
+            col = self.screen_size.col - 1
+        else:
+            col -= 1
+        SetConsoleCursorPosition(
+            self._handle, coords=WindowsCoordinates(row=row, col=col)
+        )
+
+    def hide_cursor(self) -> None:
+        """Hide the cursor"""
+        invisible_cursor = CONSOLE_CURSOR_INFO(dwSize=100, bVisible=0)
+        SetConsoleCursorInfo(self._handle, cursor_info=invisible_cursor)
+
+    def show_cursor(self) -> None:
+        """Show the cursor"""
+        visible_cursor = CONSOLE_CURSOR_INFO(dwSize=100, bVisible=1)
+        SetConsoleCursorInfo(self._handle, cursor_info=visible_cursor)
+
+    def set_title(self, title: str) -> None:
+        """Set the title of the terminal window
+
+        Args:
+            title (str): The new title of the console window
+        """
+        assert len(title) < 255, "Console title must be less than 255 characters"
+        SetConsoleTitle(title)
+
+
+if __name__ == "__main__":
+    handle = GetStdHandle()
+
+    from rich.console import Console
+
+    console = Console()
+
+    term = LegacyWindowsTerm()
+    term.set_title("Win32 Console Examples")
+
+    style = Style(color="black", bgcolor="red")
+
+    heading = Style.parse("black on green")
+
+    # Check colour output
+    console.rule("Checking colour output")
+    console.print("[on red]on red!")
+    console.print("[blue]blue!")
+    console.print("[yellow]yellow!")
+    console.print("[bold yellow]bold yellow!")
+    console.print("[bright_yellow]bright_yellow!")
+    console.print("[dim bright_yellow]dim bright_yellow!")
+    console.print("[italic cyan]italic cyan!")
+    console.print("[bold white on blue]bold white on blue!")
+    console.print("[reverse bold white on blue]reverse bold white on blue!")
+    console.print("[bold black on cyan]bold black on cyan!")
+    console.print("[black on green]black on green!")
+    console.print("[blue on green]blue on green!")
+    console.print("[white on black]white on black!")
+    console.print("[black on white]black on white!")
+    console.print("[#1BB152 on #DA812D]#1BB152 on #DA812D!")
+
+    # Check cursor movement
+    console.rule("Checking cursor movement")
+    console.print()
+    term.move_cursor_backward()
+    term.move_cursor_backward()
+    term.write_text("went back and wrapped to prev line")
+    time.sleep(1)
+    term.move_cursor_up()
+    term.write_text("we go up")
+    time.sleep(1)
+    term.move_cursor_down()
+    term.write_text("and down")
+    time.sleep(1)
+    term.move_cursor_up()
+    term.move_cursor_backward()
+    term.move_cursor_backward()
+    term.write_text("we went up and back 2")
+    time.sleep(1)
+    term.move_cursor_down()
+    term.move_cursor_backward()
+    term.move_cursor_backward()
+    term.write_text("we went down and back 2")
+    time.sleep(1)
+
+    # Check erasing of lines
+    term.hide_cursor()
+    console.print()
+    console.rule("Checking line erasing")
+    console.print("\n...Deleting to the start of the line...")
+    term.write_text("The red arrow shows the cursor location, and direction of erase")
+    time.sleep(1)
+    term.move_cursor_to_column(16)
+    term.write_styled("<", Style.parse("black on red"))
+    term.move_cursor_backward()
+    time.sleep(1)
+    term.erase_start_of_line()
+    time.sleep(1)
+
+    console.print("\n\n...And to the end of the line...")
+    term.write_text("The red arrow shows the cursor location, and direction of erase")
+    time.sleep(1)
+
+    term.move_cursor_to_column(16)
+    term.write_styled(">", Style.parse("black on red"))
+    time.sleep(1)
+    term.erase_end_of_line()
+    time.sleep(1)
+
+    console.print("\n\n...Now the whole line will be erased...")
+    term.write_styled("I'm going to disappear!", style=Style.parse("black on cyan"))
+    time.sleep(1)
+    term.erase_line()
+
+    term.show_cursor()
+    print("\n")
diff --git a/rich/_windows.py b/rich/_windows.py
index b1b30b65ec..54d834e62e 100644
--- a/rich/_windows.py
+++ b/rich/_windows.py
@@ -21,6 +21,14 @@ class WindowsConsoleFeatures:
     else:
         windll = None
         raise ImportError("Not windows")
+
+    from rich._win32_console import (
+        ENABLE_VIRTUAL_TERMINAL_PROCESSING,
+        GetConsoleMode,
+        GetStdHandle,
+        LegacyWindowsError,
+    )
+
 except (AttributeError, ImportError, ValueError):
 
     # Fallback if we can't load the Windows DLL
@@ -30,28 +38,20 @@ def get_windows_console_features() -> WindowsConsoleFeatures:
 
 else:
 
-    STDOUT = -11
-    ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4
-    _GetConsoleMode = windll.kernel32.GetConsoleMode
-    _GetConsoleMode.argtypes = [wintypes.HANDLE, wintypes.LPDWORD]
-    _GetConsoleMode.restype = wintypes.BOOL
-
-    _GetStdHandle = windll.kernel32.GetStdHandle
-    _GetStdHandle.argtypes = [
-        wintypes.DWORD,
-    ]
-    _GetStdHandle.restype = wintypes.HANDLE
-
     def get_windows_console_features() -> WindowsConsoleFeatures:
         """Get windows console features.
 
         Returns:
             WindowsConsoleFeatures: An instance of WindowsConsoleFeatures.
         """
-        handle = _GetStdHandle(STDOUT)
-        console_mode = wintypes.DWORD()
-        result = _GetConsoleMode(handle, console_mode)
-        vt = bool(result and console_mode.value & ENABLE_VIRTUAL_TERMINAL_PROCESSING)
+        handle = GetStdHandle()
+        try:
+            console_mode = GetConsoleMode(handle)
+            success = True
+        except LegacyWindowsError:
+            console_mode = 0
+            success = False
+        vt = bool(success and console_mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING)
         truecolor = False
         if vt:
             win_version = sys.getwindowsversion()
diff --git a/rich/_windows_renderer.py b/rich/_windows_renderer.py
new file mode 100644
index 0000000000..e9debe30fb
--- /dev/null
+++ b/rich/_windows_renderer.py
@@ -0,0 +1,55 @@
+from typing import Iterable, Sequence, Tuple, cast
+
+from rich._win32_console import LegacyWindowsTerm, WindowsCoordinates
+from rich.segment import ControlCode, ControlType, Segment
+
+
+def legacy_windows_render(buffer: Iterable[Segment], term: LegacyWindowsTerm) -> None:
+    """Makes appropriate Windows Console API calls based on the segments in the buffer.
+
+    Args:
+        buffer (Iterable[Segment]): Iterable of Segments to convert to Win32 API calls.
+        term (LegacyWindowsTerm): Used to call the Windows Console API.
+    """
+    for segment in buffer:
+        text, style, control = segment
+
+        if not control:
+            if style:
+                term.write_styled(text, style)
+            else:
+                term.write_text(text)
+        else:
+            control_codes: Sequence[ControlCode] = control
+            for control_code in control_codes:
+                control_type = control_code[0]
+                if control_type == ControlType.CURSOR_MOVE_TO:
+                    _, x, y = cast(Tuple[ControlType, int, int], control_code)
+                    term.move_cursor_to(WindowsCoordinates(row=y - 1, col=x - 1))
+                elif control_type == ControlType.CARRIAGE_RETURN:
+                    term.write_text("\r")
+                elif control_type == ControlType.HOME:
+                    term.move_cursor_to(WindowsCoordinates(0, 0))
+                elif control_type == ControlType.CURSOR_UP:
+                    term.move_cursor_up()
+                elif control_type == ControlType.CURSOR_DOWN:
+                    term.move_cursor_down()
+                elif control_type == ControlType.CURSOR_FORWARD:
+                    term.move_cursor_forward()
+                elif control_type == ControlType.CURSOR_BACKWARD:
+                    term.move_cursor_backward()
+                elif control_type == ControlType.CURSOR_MOVE_TO_COLUMN:
+                    _, column = cast(Tuple[ControlType, int], control_code)
+                    term.move_cursor_to_column(column - 1)
+                elif control_type == ControlType.HIDE_CURSOR:
+                    term.hide_cursor()
+                elif control_type == ControlType.SHOW_CURSOR:
+                    term.show_cursor()
+                elif control_type == ControlType.ERASE_IN_LINE:
+                    _, mode = cast(Tuple[ControlType, int], control_code)
+                    if mode == 0:
+                        term.erase_end_of_line()
+                    elif mode == 1:
+                        term.erase_start_of_line()
+                    elif mode == 2:
+                        term.erase_line()
diff --git a/rich/align.py b/rich/align.py
index 3f605a7b9d..c9b7cfa01e 100644
--- a/rich/align.py
+++ b/rich/align.py
@@ -18,7 +18,6 @@
 
 AlignMethod = Literal["left", "center", "right"]
 VerticalAlignMethod = Literal["top", "middle", "bottom"]
-AlignValues = AlignMethod  # TODO: deprecate AlignValues
 
 
 class Align(JupyterMixin):
diff --git a/rich/color.py b/rich/color.py
index f0fa026d64..b6c56a1ee4 100644
--- a/rich/color.py
+++ b/rich/color.py
@@ -7,7 +7,7 @@
 
 from ._palettes import EIGHT_BIT_PALETTE, STANDARD_PALETTE, WINDOWS_PALETTE
 from .color_triplet import ColorTriplet
-from .repr import rich_repr, Result
+from .repr import Result, rich_repr
 from .terminal_theme import DEFAULT_TERMINAL_THEME
 
 if TYPE_CHECKING:  # pragma: no cover
@@ -61,6 +61,7 @@ def __repr__(self) -> str:
     "bright_cyan": 14,
     "bright_white": 15,
     "grey0": 16,
+    "gray0": 16,
     "navy_blue": 17,
     "dark_blue": 18,
     "blue3": 20,
@@ -96,6 +97,7 @@ def __repr__(self) -> str:
     "blue_violet": 57,
     "orange4": 94,
     "grey37": 59,
+    "gray37": 59,
     "medium_purple4": 60,
     "slate_blue3": 62,
     "royal_blue1": 63,
@@ -128,7 +130,9 @@ def __repr__(self) -> str:
     "yellow4": 106,
     "wheat4": 101,
     "grey53": 102,
+    "gray53": 102,
     "light_slate_grey": 103,
+    "light_slate_gray": 103,
     "medium_purple": 104,
     "light_slate_blue": 105,
     "dark_olive_green3": 149,
@@ -155,11 +159,13 @@ def __repr__(self) -> str:
     "light_salmon3": 173,
     "rosy_brown": 138,
     "grey63": 139,
+    "gray63": 139,
     "medium_purple1": 141,
     "gold3": 178,
     "dark_khaki": 143,
     "navajo_white3": 144,
     "grey69": 145,
+    "gray69": 145,
     "light_steel_blue3": 146,
     "light_steel_blue": 147,
     "yellow3": 184,
@@ -189,6 +195,7 @@ def __repr__(self) -> str:
     "light_goldenrod2": 222,
     "light_yellow3": 187,
     "grey84": 188,
+    "gray84": 188,
     "light_steel_blue1": 189,
     "yellow2": 190,
     "dark_olive_green1": 192,
@@ -223,30 +230,55 @@ def __repr__(self) -> str:
     "wheat1": 229,
     "cornsilk1": 230,
     "grey100": 231,
+    "gray100": 231,
     "grey3": 232,
+    "gray3": 232,
     "grey7": 233,
+    "gray7": 233,
     "grey11": 234,
+    "gray11": 234,
     "grey15": 235,
+    "gray15": 235,
     "grey19": 236,
+    "gray19": 236,
     "grey23": 237,
+    "gray23": 237,
     "grey27": 238,
+    "gray27": 238,
     "grey30": 239,
+    "gray30": 239,
     "grey35": 240,
+    "gray35": 240,
     "grey39": 241,
+    "gray39": 241,
     "grey42": 242,
+    "gray42": 242,
     "grey46": 243,
+    "gray46": 243,
     "grey50": 244,
+    "gray50": 244,
     "grey54": 245,
+    "gray54": 245,
     "grey58": 246,
+    "gray58": 246,
     "grey62": 247,
+    "gray62": 247,
     "grey66": 248,
+    "gray66": 248,
     "grey70": 249,
+    "gray70": 249,
     "grey74": 250,
+    "gray74": 250,
     "grey78": 251,
+    "gray78": 251,
     "grey82": 252,
+    "gray82": 252,
     "grey85": 253,
+    "gray85": 253,
     "grey89": 254,
+    "gray89": 254,
     "grey93": 255,
+    "gray93": 255,
 }
 
 
@@ -279,8 +311,8 @@ class Color(NamedTuple):
 
     def __rich__(self) -> "Text":
         """Dispays the actual color if Rich printed."""
-        from .text import Text
         from .style import Style
+        from .text import Text
 
         return Text.assemble(
             f" "ConsoleOptions":
         options.max_height = options.height = height
         return options
 
+    def reset_height(self) -> "ConsoleOptions":
+        """Return a copy of the options with height set to ``None``.
+
+        Returns:
+            ~ConsoleOptions: New console options instance.
+        """
+        options = self.copy()
+        options.height = None
+        return options
+
     def update_dimensions(self, width: int, height: int) -> "ConsoleOptions":
         """Update the width and height, and return a copy.
 
@@ -571,12 +581,6 @@ def detect_legacy_windows() -> bool:
     return WINDOWS and not get_windows_console_features().vt
 
 
-if detect_legacy_windows():  # pragma: no cover
-    from colorama import init
-
-    init(strip=False)
-
-
 class Console:
     """A high level console interface.
 
@@ -1141,7 +1145,7 @@ def show_cursor(self, show: bool = True) -> bool:
         Args:
             show (bool, optional): Set visibility of the cursor.
         """
-        if self.is_terminal and not self.legacy_windows:
+        if self.is_terminal:
             self.control(Control.show_cursor(show))
             return True
         return False
@@ -1251,6 +1255,7 @@ def render(
                 f"object {render_iterable!r} is not renderable"
             )
         _Segment = Segment
+        _options = _options.reset_height()
         for render_output in iter_render:
             if isinstance(render_output, _Segment):
                 yield render_output
@@ -1916,22 +1921,41 @@ def _check_buffer(self) -> None:
                     display(self._buffer, self._render_buffer(self._buffer[:]))
                     del self._buffer[:]
                 else:
-                    text = self._render_buffer(self._buffer[:])
-                    del self._buffer[:]
-                    if text:
+                    if WINDOWS:
                         try:
-                            if WINDOWS:  # pragma: no cover
-                                # https://bugs.python.org/issue37871
-                                write = self.file.write
-                                for line in text.splitlines(True):
+                            file_no = self.file.fileno()
+                        except (ValueError, io.UnsupportedOperation):
+                            file_no = -1
+
+                        legacy_windows_stdout = self.legacy_windows and file_no == 1
+                        if legacy_windows_stdout:
+                            from rich._win32_console import LegacyWindowsTerm
+                            from rich._windows_renderer import legacy_windows_render
+
+                            legacy_windows_render(self._buffer[:], LegacyWindowsTerm())
+
+                        output_capture_enabled = bool(self._buffer_index)
+                        if not legacy_windows_stdout or output_capture_enabled:
+                            text = self._render_buffer(self._buffer[:])
+                            # https://bugs.python.org/issue37871
+                            write = self.file.write
+                            for line in text.splitlines(True):
+                                try:
                                     write(line)
-                            else:
-                                self.file.write(text)
-                            self.file.flush()
+                                except UnicodeEncodeError as error:
+                                    error.reason = f"{error.reason}\n*** You may need to add PYTHONIOENCODING=utf-8 to your environment ***"
+                                    raise
+                    else:
+                        text = self._render_buffer(self._buffer[:])
+                        try:
+                            self.file.write(text)
                         except UnicodeEncodeError as error:
                             error.reason = f"{error.reason}\n*** You may need to add PYTHONIOENCODING=utf-8 to your environment ***"
                             raise
 
+                    self.file.flush()
+                    del self._buffer[:]
+
     def _render_buffer(self, buffer: Iterable[Segment]) -> str:
         """Render buffered output, and clear buffer."""
         output: List[str] = []
@@ -1982,23 +2006,15 @@ def input(
         Returns:
             str: Text read from stdin.
         """
-        prompt_str = ""
         if prompt:
-            with self.capture() as capture:
-                self.print(prompt, markup=markup, emoji=emoji, end="")
-            prompt_str = capture.get()
-        if self.legacy_windows:
-            # Legacy windows doesn't like ANSI codes in getpass or input (colorama bug)?
-            self.file.write(prompt_str)
-            prompt_str = ""
+            self.print(prompt, markup=markup, emoji=emoji, end="")
         if password:
-            result = getpass(prompt_str, stream=stream)
+            result = getpass("", stream=stream)
         else:
             if stream:
-                self.file.write(prompt_str)
                 result = stream.readline()
             else:
-                result = input(prompt_str)
+                result = input()
         return result
 
     def export_text(self, *, clear: bool = True, styles: bool = False) -> str:
@@ -2060,8 +2076,8 @@ def export_html(
         Args:
             theme (TerminalTheme, optional): TerminalTheme object containing console colors.
             clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``.
-            code_format (str, optional): Format string to render HTML, should contain {foreground}
-                {background} and {code}.
+            code_format (str, optional): Format string to render HTML. In addition to '{foreground}',
+                '{background}', and '{code}', should contain '{stylesheet}' if inline_styles is ``False``.
             inline_styles (bool, optional): If ``True`` styles will be inlined in to spans, which makes files
                 larger but easier to cut and paste markup. If ``False``, styles will be embedded in a style tag.
                 Defaults to False.
@@ -2137,8 +2153,8 @@ def save_html(
             path (str): Path to write html file.
             theme (TerminalTheme, optional): TerminalTheme object containing console colors.
             clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``.
-            code_format (str, optional): Format string to render HTML, should contain {foreground}
-                {background} and {code}.
+            code_format (str, optional): Format string to render HTML. In addition to '{foreground}',
+                '{background}', and '{code}', should contain '{stylesheet}' if inline_styles is ``False``.
             inline_styles (bool, optional): If ``True`` styles will be inlined in to spans, which makes files
                 larger but easier to cut and paste markup. If ``False``, styles will be embedded in a style tag.
                 Defaults to False.
@@ -2209,3 +2225,5 @@ def save_html(
         }
     )
     console.log("foo")
+
+    console.print_json(data={"name": "apple", "count": 1}, indent=None)
diff --git a/rich/diagnose.py b/rich/diagnose.py
index 455e11dc02..09c4e0622b 100644
--- a/rich/diagnose.py
+++ b/rich/diagnose.py
@@ -1,6 +1,35 @@
-if __name__ == "__main__":  # pragma: no cover
-    from rich.console import Console
-    from rich import inspect
+import os
+import platform
+
+from rich import inspect
+from rich.console import Console, get_windows_console_features
+from rich.panel import Panel
+from rich.pretty import Pretty
 
+
+def report() -> None:  # pragma: no cover
+    """Print a report to the terminal with debugging information"""
     console = Console()
     inspect(console)
+    features = get_windows_console_features()
+    inspect(features)
+
+    env_names = (
+        "TERM",
+        "COLORTERM",
+        "CLICOLOR",
+        "NO_COLOR",
+        "TERM_PROGRAM",
+        "COLUMNS",
+        "LINES",
+        "JPY_PARENT_PID",
+        "VSCODE_VERBOSE_LOGGING",
+    )
+    env = {name: os.getenv(name) for name in env_names}
+    console.print(Panel.fit((Pretty(env)), title="[b]Environment Variables"))
+
+    console.print(f'platform="{platform.system()}"')
+
+
+if __name__ == "__main__":  # pragma: no cover
+    report()
diff --git a/rich/filesize.py b/rich/filesize.py
index b3a0996b05..61be47510f 100644
--- a/rich/filesize.py
+++ b/rich/filesize.py
@@ -13,7 +13,7 @@
 
 __all__ = ["decimal"]
 
-from typing import Iterable, List, Tuple, Optional
+from typing import Iterable, List, Optional, Tuple
 
 
 def _to_str(
@@ -30,7 +30,7 @@ def _to_str(
         return "{:,} bytes".format(size)
 
     for i, suffix in enumerate(suffixes, 2):  # noqa: B007
-        unit = base ** i
+        unit = base**i
         if size < unit:
             break
     return "{:,.{precision}f}{separator}{}".format(
@@ -44,7 +44,7 @@ def _to_str(
 def pick_unit_and_suffix(size: int, suffixes: List[str], base: int) -> Tuple[int, str]:
     """Pick a suffix and base for the given size."""
     for i, suffix in enumerate(suffixes):
-        unit = base ** i
+        unit = base**i
         if size < unit * base:
             break
     return unit, suffix
diff --git a/rich/highlighter.py b/rich/highlighter.py
index 8afdd017b6..6e10d56d7e 100644
--- a/rich/highlighter.py
+++ b/rich/highlighter.py
@@ -1,7 +1,9 @@
+import re
+import string
 from abc import ABC, abstractmethod
 from typing import List, Union
 
-from .text import Text
+from .text import Span, Text
 
 
 def _combine_regex(*regexes: str) -> str:
@@ -81,22 +83,22 @@ class ReprHighlighter(RegexHighlighter):
 
     base_style = "repr."
     highlights = [
-        r"(?P\<)(?P[\w\-\.\:]*)(?P[\w\W]*?)(?P\>)",
-        r"(?P[\w_]{1,50})=(?P\"?[\w_]+\"?)?",
-        r"(?P[\{\[\(\)\]\}])",
+        r"(?P<)(?P[-\w.:|]*)(?P[\w\W]*?)(?P>)",
+        r'(?P[\w_]{1,50})=(?P"?[\w_]+"?)?',
+        r"(?P[][{}()])",
         _combine_regex(
             r"(?P[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})",
             r"(?P([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4})",
             r"(?P(?:[0-9A-Fa-f]{1,2}-){7}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{1,2}:){7}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{4}\.){3}[0-9A-Fa-f]{4})",
             r"(?P(?:[0-9A-Fa-f]{1,2}-){5}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{1,2}:){5}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{4}\.){2}[0-9A-Fa-f]{4})",
-            r"(?P[\w\.]*?)\(",
+            r"(?P[\w.]*?)\(",
             r"\b(?PTrue)\b|\b(?PFalse)\b|\b(?PNone)\b",
             r"(?P\.\.\.)",
-            r"(?P(?\B(\/[\w\.\-\_\+]+)*\/)(?P[\w\.\-\_\+]*)?",
-            r"(?b?\'\'\'.*?(?[a-fA-F0-9]{8}\-[a-fA-F0-9]{4}\-[a-fA-F0-9]{4}\-[a-fA-F0-9]{4}\-[a-fA-F0-9]{12})",
-            r"(?P(file|https|http|ws|wss):\/\/[0-9a-zA-Z\$\-\_\+\!`\(\)\,\.\?\/\;\:\&\=\%\#]*)",
+            r"(?P(?\B(/[-\w._+]+)*\/)(?P[-\w._+]*)?",
+            r"(?b?'''.*?(?[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})",
+            r"(?P(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#]*)",
         ),
     ]
 
@@ -104,17 +106,39 @@ class ReprHighlighter(RegexHighlighter):
 class JSONHighlighter(RegexHighlighter):
     """Highlights JSON"""
 
+    # Captures the start and end of JSON strings, handling escaped quotes
+    JSON_STR = r"(?b?\".*?(?[\{\[\(\)\]\}])",
             r"\b(?Ptrue)\b|\b(?Pfalse)\b|\b(?Pnull)\b",
             r"(?P(?b?\".*?(?b?\".*?(? None:
+        super().highlight(text)
+
+        # Additional work to handle highlighting JSON keys
+        plain = text.plain
+        append = text.spans.append
+        whitespace = self.JSON_WHITESPACE
+        for match in re.finditer(self.JSON_STR, plain):
+            start, end = match.span()
+            cursor = end
+            while cursor < len(plain):
+                char = plain[cursor]
+                cursor += 1
+                if char == ":":
+                    append(Span(start, end, "json.key"))
+                elif char in whitespace:
+                    continue
+                break
+
 
 if __name__ == "__main__":  # pragma: no cover
     from .console import Console
@@ -145,3 +169,6 @@ class JSONHighlighter(RegexHighlighter):
     console.print(
         "127.0.1.1 bar 192.168.1.4 2001:0db8:85a3:0000:0000:8a2e:0370:7334 foo"
     )
+    import json
+
+    console.print_json(json.dumps(obj={"name": "apple", "count": 1}), indent=None)
diff --git a/rich/jupyter.py b/rich/jupyter.py
index bedf5cb19a..97340d004a 100644
--- a/rich/jupyter.py
+++ b/rich/jupyter.py
@@ -63,7 +63,7 @@ def escape(text: str) -> str:
             rule = style.get_html_style(theme)
             text = f'{text}' if rule else text
             if style.link:
-                text = f'{text}'
+                text = f'{text}'
         append_fragment(text)
 
     code = "".join(fragments)
diff --git a/rich/layout.py b/rich/layout.py
index e25ced675c..918e631c2f 100644
--- a/rich/layout.py
+++ b/rich/layout.py
@@ -73,6 +73,7 @@ def __rich_console__(
             style=self.style,
             title=self.highlighter(title),
             border_style="blue",
+            height=height,
         )
 
 
diff --git a/rich/logging.py b/rich/logging.py
index 002f1f7bf1..58188fd8a8 100644
--- a/rich/logging.py
+++ b/rich/logging.py
@@ -2,7 +2,8 @@
 from datetime import datetime
 from logging import Handler, LogRecord
 from pathlib import Path
-from typing import ClassVar, List, Optional, Type, Union
+from types import ModuleType
+from typing import ClassVar, List, Optional, Iterable, Type, Union
 
 from . import get_console
 from ._log_render import LogRender, FormatTimeCallable
@@ -37,10 +38,12 @@ class RichHandler(Handler):
         tracebacks_theme (str, optional): Override pygments theme used in traceback.
         tracebacks_word_wrap (bool, optional): Enable word wrapping of long tracebacks lines. Defaults to True.
         tracebacks_show_locals (bool, optional): Enable display of locals in tracebacks. Defaults to False.
+        tracebacks_suppress (Sequence[Union[str, ModuleType]]): Optional sequence of modules or paths to exclude from traceback.
         locals_max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
             Defaults to 10.
         locals_max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to 80.
         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 %X] ".
+        keywords (List[str], optional): List of words to highlight instead of ``RichHandler.KEYWORDS``.
     """
 
     KEYWORDS: ClassVar[Optional[List[str]]] = [
@@ -73,9 +76,11 @@ def __init__(
         tracebacks_theme: Optional[str] = None,
         tracebacks_word_wrap: bool = True,
         tracebacks_show_locals: bool = False,
+        tracebacks_suppress: Iterable[Union[str, ModuleType]] = (),
         locals_max_length: int = 10,
         locals_max_string: int = 80,
         log_time_format: Union[str, FormatTimeCallable] = "[%x %X]",
+        keywords: Optional[List[str]] = None,
     ) -> None:
         super().__init__(level=level)
         self.console = console or get_console()
@@ -96,8 +101,10 @@ def __init__(
         self.tracebacks_theme = tracebacks_theme
         self.tracebacks_word_wrap = tracebacks_word_wrap
         self.tracebacks_show_locals = tracebacks_show_locals
+        self.tracebacks_suppress = tracebacks_suppress
         self.locals_max_length = locals_max_length
         self.locals_max_string = locals_max_string
+        self.keywords = keywords
 
     def get_level_text(self, record: LogRecord) -> Text:
         """Get the level name from the record.
@@ -137,6 +144,7 @@ def emit(self, record: LogRecord) -> None:
                 show_locals=self.tracebacks_show_locals,
                 locals_max_length=self.locals_max_length,
                 locals_max_string=self.locals_max_string,
+                suppress=self.tracebacks_suppress,
             )
             message = record.getMessage()
             if self.formatter:
@@ -171,8 +179,12 @@ def render_message(self, record: LogRecord, message: str) -> "ConsoleRenderable"
         if highlighter:
             message_text = highlighter(message_text)
 
-        if self.KEYWORDS:
-            message_text.highlight_words(self.KEYWORDS, "logging.keyword")
+        if self.keywords is None:
+            self.keywords = self.KEYWORDS
+
+        if self.keywords:
+            message_text.highlight_words(self.keywords, "logging.keyword")
+
         return message_text
 
     def render(
diff --git a/rich/markdown.py b/rich/markdown.py
index 92d0d3c010..186e103697 100644
--- a/rich/markdown.py
+++ b/rich/markdown.py
@@ -25,7 +25,7 @@ def create(cls, markdown: "Markdown", node: Any) -> "MarkdownElement":
         """Factory to create markdown element,
 
         Args:
-            markdown (Markdown): THe parent Markdown object.
+            markdown (Markdown): The parent Markdown object.
             node (Any): A node from Pygments.
 
         Returns:
@@ -312,7 +312,7 @@ def create(cls, markdown: "Markdown", node: Any) -> "MarkdownElement":
         """Factory to create markdown element,
 
         Args:
-            markdown (Markdown): THe parent Markdown object.
+            markdown (Markdown): The parent Markdown object.
             node (Any): A node from Pygments.
 
         Returns:
diff --git a/rich/markup.py b/rich/markup.py
index 58903f6bbb..a0201dfe2e 100644
--- a/rich/markup.py
+++ b/rich/markup.py
@@ -1,21 +1,20 @@
+import re
 from ast import literal_eval
 from operator import attrgetter
-import re
 from typing import Callable, Iterable, List, Match, NamedTuple, Optional, Tuple, Union
 
+from ._emoji_replace import _emoji_replace
+from .emoji import EmojiVariant
 from .errors import MarkupError
 from .style import Style
 from .text import Span, Text
-from .emoji import EmojiVariant
-from ._emoji_replace import _emoji_replace
-
 
 RE_TAGS = re.compile(
-    r"""((\\*)\[([a-z#\/@].*?)\])""",
+    r"""((\\*)\[([a-z#/@][^[]*?)])""",
     re.VERBOSE,
 )
 
-RE_HANDLER = re.compile(r"^([\w\.]*?)(\(.*?\))?$")
+RE_HANDLER = re.compile(r"^([\w.]*?)(\(.*?\))?$")
 
 
 class Tag(NamedTuple):
@@ -146,6 +145,8 @@ def pop_style(style_name: str) -> Tuple[int, Tag]:
 
     for position, plain_text, tag in _parse(markup):
         if plain_text is not None:
+            # Handle open brace escapes, where the brace is not part of a tag.
+            plain_text = plain_text.replace("\\[", "[")
             append(emoji_replace(plain_text) if emoji else plain_text)
         elif tag is not None:
             if tag.name.startswith("/"):  # Closing tag
@@ -233,8 +234,8 @@ def pop_style(style_name: str) -> Tuple[int, Tag]:
         ":warning-emoji: [bold red blink] DANGER![/]",
     ]
 
-    from rich.table import Table
     from rich import print
+    from rich.table import Table
 
     grid = Table("Markup", "Result", padding=(0, 1))
 
diff --git a/rich/measure.py b/rich/measure.py
index aea238df93..e12787c8be 100644
--- a/rich/measure.py
+++ b/rich/measure.py
@@ -1,5 +1,5 @@
 from operator import itemgetter
-from typing import Callable, Iterable, NamedTuple, Optional, TYPE_CHECKING
+from typing import TYPE_CHECKING, Callable, Iterable, NamedTuple, Optional
 
 from . import errors
 from .protocol import is_renderable, rich_cast
@@ -96,7 +96,9 @@ def get(
         if _max_width < 1:
             return Measurement(0, 0)
         if isinstance(renderable, str):
-            renderable = console.render_str(renderable, markup=options.markup)
+            renderable = console.render_str(
+                renderable, markup=options.markup, highlight=False
+            )
         renderable = rich_cast(renderable)
         if is_renderable(renderable):
             get_console_width: Optional[
diff --git a/rich/pretty.py b/rich/pretty.py
index f42434ef5c..57f3e62e2c 100644
--- a/rich/pretty.py
+++ b/rich/pretty.py
@@ -1,4 +1,5 @@
 import builtins
+import collections
 import dataclasses
 import inspect
 import os
@@ -30,7 +31,6 @@
 except ImportError:  # pragma: no cover
     _attr_module = None  # type: ignore
 
-
 from . import get_console
 from ._loop import loop_last
 from ._pick import pick_bool
@@ -79,6 +79,29 @@ def _is_dataclass_repr(obj: object) -> bool:
         return False
 
 
+_dummy_namedtuple = collections.namedtuple("_dummy_namedtuple", [])
+
+
+def _has_default_namedtuple_repr(obj: object) -> bool:
+    """Check if an instance of namedtuple contains the default repr
+
+    Args:
+        obj (object): A namedtuple
+
+    Returns:
+        bool: True if the default repr is used, False if there's a custom repr.
+    """
+    obj_file = None
+    try:
+        obj_file = inspect.getfile(obj.__repr__)
+    except (OSError, TypeError):
+        # OSError handles case where object is defined in __main__ scope, e.g. REPL - no filename available.
+        # TypeError trapped defensively, in case of object without filename slips through.
+        pass
+    default_repr_file = inspect.getfile(_dummy_namedtuple.__repr__)
+    return obj_file == default_repr_file
+
+
 def _ipy_display_hook(
     value: Any,
     console: Optional["Console"] = None,
@@ -323,6 +346,7 @@ def __rich_measure__(
             indent_size=self.indent_size,
             max_length=self.max_length,
             max_string=self.max_string,
+            expand_all=self.expand_all,
         )
         text_width = (
             max(cell_len(line) for line in pretty_str.splitlines()) if pretty_str else 0
@@ -382,6 +406,7 @@ class Node:
     empty: str = ""
     last: bool = False
     is_tuple: bool = False
+    is_namedtuple: bool = False
     children: Optional[List["Node"]] = None
     key_separator = ": "
     separator: str = ", "
@@ -396,7 +421,7 @@ def iter_tokens(self) -> Iterable[str]:
         elif self.children is not None:
             if self.children:
                 yield self.open_brace
-                if self.is_tuple and len(self.children) == 1:
+                if self.is_tuple and not self.is_namedtuple and len(self.children) == 1:
                     yield from self.children[0].iter_tokens()
                     yield ","
                 else:
@@ -523,6 +548,25 @@ def __str__(self) -> str:
             )
 
 
+def _is_namedtuple(obj: Any) -> bool:
+    """Checks if an object is most likely a namedtuple. It is possible
+    to craft an object that passes this check and isn't a namedtuple, but
+    there is only a minuscule chance of this happening unintentionally.
+
+    Args:
+        obj (Any): The object to test
+
+    Returns:
+        bool: True if the object is a namedtuple. False otherwise.
+    """
+    try:
+        fields = getattr(obj, "_fields", None)
+    except Exception:
+        # Being very defensive - if we cannot get the attr then its not a namedtuple
+        return False
+    return isinstance(obj, tuple) and isinstance(fields, tuple)
+
+
 def traverse(
     _object: Any,
     max_length: Optional[int] = None,
@@ -730,7 +774,25 @@ def iter_attrs() -> Iterable[
                     append(child_node)
 
                 pop_visited(obj_id)
-
+        elif _is_namedtuple(obj) and _has_default_namedtuple_repr(obj):
+            if reached_max_depth:
+                node = Node(value_repr="...")
+            else:
+                children = []
+                class_name = obj.__class__.__name__
+                node = Node(
+                    open_brace=f"{class_name}(",
+                    close_brace=")",
+                    children=children,
+                    empty=f"{class_name}()",
+                )
+                append = children.append
+                for last, (key, value) in loop_last(obj._asdict().items()):
+                    child_node = _traverse(value, depth=depth + 1)
+                    child_node.key_repr = key
+                    child_node.last = last
+                    child_node.key_separator = "="
+                    append(child_node)
         elif _safe_isinstance(obj, _CONTAINERS):
             for container_type in _CONTAINERS:
                 if _safe_isinstance(obj, container_type):
@@ -779,7 +841,7 @@ def iter_attrs() -> Iterable[
                         child_node.last = index == last_item_index
                         append(child_node)
                 if max_length is not None and num_items > max_length:
-                    append(Node(value_repr=f"... +{num_items-max_length}", last=True))
+                    append(Node(value_repr=f"... +{num_items - max_length}", last=True))
             else:
                 node = Node(empty=empty, children=[], last=root)
 
@@ -787,6 +849,7 @@ def iter_attrs() -> Iterable[
         else:
             node = Node(value_repr=to_repr(obj), last=root)
         node.is_tuple = _safe_isinstance(obj, tuple)
+        node.is_namedtuple = _is_namedtuple(obj)
         return node
 
     node = _traverse(_object, root=True)
@@ -877,6 +940,15 @@ def __repr__(self) -> str:
             1 / 0
             return "this will fail"
 
+    from typing import NamedTuple
+
+    class StockKeepingUnit(NamedTuple):
+        name: str
+        description: str
+        price: float
+        category: str
+        reviews: List[str]
+
     d = defaultdict(int)
     d["foo"] = 5
     data = {
@@ -903,6 +975,13 @@ def __repr__(self) -> str:
             ]
         ),
         "atomic": (False, True, None),
+        "namedtuple": StockKeepingUnit(
+            "Sparkling British Spring Water",
+            "Carbonated spring water",
+            0.9,
+            "water",
+            ["its amazing!", "its terrible!"],
+        ),
         "Broken": BrokenRepr(),
     }
     data["foo"].append(data)  # type: ignore
diff --git a/rich/progress.py b/rich/progress.py
index 1f670db438..e4abbdb66a 100644
--- a/rich/progress.py
+++ b/rich/progress.py
@@ -343,18 +343,48 @@ def render(self, task: "Task") -> Text:
 
 
 class TimeRemainingColumn(ProgressColumn):
-    """Renders estimated time remaining."""
+    """Renders estimated time remaining.
+
+    Args:
+        compact (bool, optional): Render MM:SS when time remaining is less than an hour. Defaults to False.
+        elapsed_when_finished (bool, optional): Render time elapsed when the task is finished. Defaults to False.
+    """
 
     # Only refresh twice a second to prevent jitter
     max_refresh = 0.5
 
+    def __init__(
+        self,
+        compact: bool = False,
+        elapsed_when_finished: bool = False,
+        table_column: Optional[Column] = None,
+    ):
+        self.compact = compact
+        self.elapsed_when_finished = elapsed_when_finished
+        super().__init__(table_column=table_column)
+
     def render(self, task: "Task") -> Text:
         """Show time remaining."""
-        remaining = task.time_remaining
-        if remaining is None:
-            return Text("-:--:--", style="progress.remaining")
-        remaining_delta = timedelta(seconds=int(remaining))
-        return Text(str(remaining_delta), style="progress.remaining")
+        if self.elapsed_when_finished and task.finished:
+            task_time = task.finished_time
+            style = "progress.elapsed"
+        else:
+            task_time = task.time_remaining
+            style = "progress.remaining"
+
+        if task_time is None:
+            return Text("--:--" if self.compact else "-:--:--", style=style)
+
+        # Based on https://github.com/tqdm/tqdm/blob/master/tqdm/std.py
+        minutes, seconds = divmod(int(task_time), 60)
+        hours, minutes = divmod(minutes, 60)
+
+        if self.compact and not hours:
+            formatted = f"{minutes:02d}:{seconds:02d}"
+        else:
+            formatted = f"{hours:d}:{minutes:02d}:{seconds:02d}"
+
+        return Text(formatted, style=style)
 
 
 class FileSizeColumn(ProgressColumn):
@@ -375,6 +405,33 @@ def render(self, task: "Task") -> Text:
         return Text(data_size, style="progress.filesize.total")
 
 
+class MofNCompleteColumn(ProgressColumn):
+    """Renders completed count/total, e.g. '  10/1000'.
+
+    Best for bounded tasks with int quantities.
+
+    Space pads the completed count so that progress length does not change as task progresses
+    past powers of 10.
+
+    Args:
+        separator (str, optional): Text to separate completed and total values. Defaults to "/".
+    """
+
+    def __init__(self, separator: str = "/", table_column: Optional[Column] = None):
+        self.separator = separator
+        super().__init__(table_column=table_column)
+
+    def render(self, task: "Task") -> Text:
+        """Show completed/total."""
+        completed = int(task.completed)
+        total = int(task.total)
+        total_width = len(str(total))
+        return Text(
+            f"{completed:{total_width}d}{self.separator}{total}",
+            style="progress.download",
+        )
+
+
 class DownloadColumn(ProgressColumn):
     """Renders file size downloaded and total, e.g. '0.5/2.3 GB'.
 
@@ -400,7 +457,9 @@ def render(self, task: "Task") -> Text:
             )
         else:
             unit, suffix = filesize.pick_unit_and_suffix(
-                total, ["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"], 1000
+                total,
+                ["bytes", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"],
+                1000,
             )
         completed_ratio = completed / unit
         total_ratio = total / unit
@@ -475,7 +534,7 @@ class Task:
     """Optional[float]: The last speed for a finished task."""
 
     _progress: Deque[ProgressSample] = field(
-        default_factory=deque, init=False, repr=False
+        default_factory=lambda: deque(maxlen=1000), init=False, repr=False
     )
 
     _lock: RLock = field(repr=False, default_factory=RLock)
@@ -588,12 +647,7 @@ def __init__(
             refresh_per_second is None or refresh_per_second > 0
         ), "refresh_per_second must be > 0"
         self._lock = RLock()
-        self.columns = columns or (
-            TextColumn("[progress.description]{task.description}"),
-            BarColumn(),
-            TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
-            TimeRemainingColumn(),
-        )
+        self.columns = columns or self.get_default_columns()
         self.speed_estimate_period = speed_estimate_period
 
         self.disable = disable
@@ -613,6 +667,37 @@ def __init__(
         self.print = self.console.print
         self.log = self.console.log
 
+    @classmethod
+    def get_default_columns(cls) -> Tuple[ProgressColumn, ...]:
+        """Get the default columns used for a new Progress instance:
+           - a text column for the description (TextColumn)
+           - the bar itself (BarColumn)
+           - a text column showing completion percentage (TextColumn)
+           - an estimated-time-remaining column (TimeRemainingColumn)
+        If the Progress instance is created without passing a columns argument,
+        the default columns defined here will be used.
+
+        You can also create a Progress instance using custom columns before
+        and/or after the defaults, as in this example:
+
+            progress = Progress(
+                SpinnerColumn(),
+                *Progress.default_columns(),
+                "Elapsed:",
+                TimeElapsedColumn(),
+            )
+
+        This code shows the creation of a Progress display, containing
+        a spinner to the left, the default columns, and a labeled elapsed
+        time column.
+        """
+        return (
+            TextColumn("[progress.description]{task.description}"),
+            BarColumn(),
+            TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
+            TimeRemainingColumn(),
+        )
+
     @property
     def console(self) -> Console:
         return self.live.console
@@ -787,8 +872,6 @@ def update(
             popleft = _progress.popleft
             while _progress and _progress[0].timestamp < old_sample_time:
                 popleft()
-            while len(_progress) > 1000:
-                popleft()
             if update_completed > 0:
                 _progress.append(ProgressSample(current_time, update_completed))
             if task.completed >= task.total and task.finished_time is None:
@@ -1015,10 +1098,7 @@ def remove_task(self, task_id: TaskID) -> None:
 
     with Progress(
         SpinnerColumn(),
-        TextColumn("[progress.description]{task.description}"),
-        BarColumn(),
-        TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
-        TimeRemainingColumn(),
+        *Progress.get_default_columns(),
         TimeElapsedColumn(),
         console=console,
         transient=True,
diff --git a/rich/segment.py b/rich/segment.py
index 97679cefc0..eb44acf11a 100644
--- a/rich/segment.py
+++ b/rich/segment.py
@@ -64,7 +64,7 @@ class Segment(NamedTuple):
     Args:
         text (str): A piece of text.
         style (:class:`~rich.style.Style`, optional): An optional style to apply to the text.
-        control (Tuple[ControlCode..], optional): Optional sequence of control codes.
+        control (Sequence[ControlCode], optional): Optional sequence of control codes.
     """
 
     text: str = ""
diff --git a/rich/syntax.py b/rich/syntax.py
index 142f48f46a..6a337e4070 100644
--- a/rich/syntax.py
+++ b/rich/syntax.py
@@ -735,7 +735,7 @@ def __rich_console__(
     parser.add_argument(
         "-x",
         "--lexer",
-        default="default",
+        default=None,
         dest="lexer_name",
         help="Lexer name",
     )
diff --git a/rich/tabulate.py b/rich/tabulate.py
deleted file mode 100644
index ca4fe293a6..0000000000
--- a/rich/tabulate.py
+++ /dev/null
@@ -1,51 +0,0 @@
-from collections.abc import Mapping
-from typing import Any, Optional
-import warnings
-
-from rich.console import JustifyMethod
-
-from . import box
-from .highlighter import ReprHighlighter
-from .pretty import Pretty
-from .table import Table
-
-
-def tabulate_mapping(
-    mapping: "Mapping[Any, Any]",
-    title: Optional[str] = None,
-    caption: Optional[str] = None,
-    title_justify: Optional[JustifyMethod] = None,
-    caption_justify: Optional[JustifyMethod] = None,
-) -> Table:
-    """Generate a simple table from a mapping.
-
-    Args:
-        mapping (Mapping): A mapping object (e.g. a dict);
-        title (str, optional): Optional title to be displayed over the table.
-        caption (str, optional): Optional caption to be displayed below the table.
-        title_justify (str, optional): Justify method for title. Defaults to None.
-        caption_justify (str, optional): Justify method for caption. Defaults to None.
-
-    Returns:
-        Table: A table instance which may be rendered by the Console.
-    """
-    warnings.warn("tabulate_mapping will be deprecated in Rich v11", DeprecationWarning)
-    table = Table(
-        show_header=False,
-        title=title,
-        caption=caption,
-        box=box.ROUNDED,
-        border_style="blue",
-    )
-    table.title = title
-    table.caption = caption
-    if title_justify is not None:
-        table.title_justify = title_justify
-    if caption_justify is not None:
-        table.caption_justify = caption_justify
-    highlighter = ReprHighlighter()
-    for key, value in mapping.items():
-        table.add_row(
-            Pretty(key, highlighter=highlighter), Pretty(value, highlighter=highlighter)
-        )
-    return table
diff --git a/rich/traceback.py b/rich/traceback.py
index 1d3b71ea32..bebf22f3ed 100644
--- a/rich/traceback.py
+++ b/rich/traceback.py
@@ -390,9 +390,8 @@ def safe_str(_object: Any) -> str:
                 exc_type = cause.__class__
                 exc_value = cause
                 traceback = cause.__traceback__
-                if traceback:
-                    is_cause = True
-                    continue
+                is_cause = True
+                continue
 
             cause = exc_value.__context__
             if (
@@ -403,9 +402,8 @@ def safe_str(_object: Any) -> str:
                 exc_type = cause.__class__
                 exc_value = cause
                 traceback = cause.__traceback__
-                if traceback:
-                    is_cause = False
-                    continue
+                is_cause = False
+                continue
             # No cover, code is reached but coverage doesn't recognize it.
             break  # pragma: no cover
 
diff --git a/rich/tree.py b/rich/tree.py
index 66203e693e..0beab7a9e5 100644
--- a/rich/tree.py
+++ b/rich/tree.py
@@ -136,6 +136,7 @@ def make_guide(index: int, style: Style) -> Segment:
                     highlight=self.highlight,
                     height=None,
                 ),
+                pad=options.justify is not None,
             )
 
             if not (depth == 0 and self.hide_root):
@@ -214,9 +215,9 @@ def __rich_measure__(
 
     code = """\
 class Segment(NamedTuple):
-    text: str = ""    
-    style: Optional[Style] = None    
-    is_control: bool = False    
+    text: str = ""
+    style: Optional[Style] = None
+    is_control: bool = False
 """
     syntax = Syntax(code, "python", theme="monokai", line_numbers=True)
 
@@ -224,7 +225,7 @@ class Segment(NamedTuple):
         """\
 ### example.md
 > Hello, World!
-> 
+>
 > Markdown _all_ the things
 """
     )
@@ -246,4 +247,5 @@ class Segment(NamedTuple):
     containers_node.add(Group("📄 [b magenta]Table", table))
 
     console = Console()
+
     console.print(root)
diff --git a/tests/test_console.py b/tests/test_console.py
index 0989d8ba16..701ccf7402 100644
--- a/tests/test_console.py
+++ b/tests/test_console.py
@@ -14,8 +14,8 @@
     Console,
     ConsoleDimensions,
     ConsoleOptions,
-    group,
     ScreenUpdate,
+    group,
 )
 from rich.control import Control
 from rich.measure import measure_renderables
@@ -185,6 +185,15 @@ def test_print_json_ensure_ascii():
     assert result == expected
 
 
+def test_print_json_indent_none():
+    console = Console(file=io.StringIO(), color_system="truecolor")
+    data = {"name": "apple", "count": 1}
+    console.print_json(data=data, indent=None)
+    result = console.file.getvalue()
+    expected = '\x1b[1m{\x1b[0m\x1b[1;34m"name"\x1b[0m: \x1b[32m"apple"\x1b[0m, \x1b[1;34m"count"\x1b[0m: \x1b[1;36m1\x1b[0m\x1b[1m}\x1b[0m\n'
+    assert result == expected
+
+
 def test_log():
     console = Console(
         file=io.StringIO(),
@@ -299,7 +308,7 @@ def test_capture():
 
 
 def test_input(monkeypatch, capsys):
-    def fake_input(prompt):
+    def fake_input(prompt=""):
         console.file.write(prompt)
         return "bar"
 
@@ -310,18 +319,6 @@ def fake_input(prompt):
     assert user_input == "bar"
 
 
-def test_input_legacy_windows(monkeypatch, capsys):
-    def fake_input(prompt):
-        console.file.write(prompt)
-        return "bar"
-
-    monkeypatch.setattr("builtins.input", fake_input)
-    console = Console(legacy_windows=True)
-    user_input = console.input(prompt="foo:")
-    assert capsys.readouterr().out == "foo:"
-    assert user_input == "bar"
-
-
 def test_input_password(monkeypatch, capsys):
     def fake_input(prompt, stream=None):
         console.file.write(prompt)
@@ -759,3 +756,28 @@ def _mock_isatty():
 def test_detect_color_system():
     console = Console(_environ={"TERM": "rxvt-unicode-256color"}, force_terminal=True)
     assert console._detect_color_system() == ColorSystem.EIGHT_BIT
+
+
+def test_reset_height():
+    """Test height is reset when rendering complex renderables."""
+    # https://github.com/Textualize/rich/issues/2042
+    class Panels:
+        def __rich_console__(self, console, options):
+            yield Panel("foo")
+            yield Panel("bar")
+
+    console = Console(
+        force_terminal=True,
+        color_system="truecolor",
+        width=20,
+        height=40,
+        legacy_windows=False,
+    )
+
+    with console.capture() as capture:
+        console.print(Panel(Panels()), height=12)
+    result = capture.get()
+    print(repr(result))
+    expected = "╭──────────────────╮\n│ ╭──────────────╮ │\n│ │ foo          │ │\n│ ╰──────────────╯ │\n│ ╭──────────────╮ │\n│ │ bar          │ │\n│ ╰──────────────╯ │\n│                  │\n│                  │\n│                  │\n│                  │\n╰──────────────────╯\n"
+
+    assert result == expected
diff --git a/tests/test_highlighter.py b/tests/test_highlighter.py
index 99793723c8..a34334e3d3 100644
--- a/tests/test_highlighter.py
+++ b/tests/test_highlighter.py
@@ -1,8 +1,10 @@
 """Tests for the highlighter classes."""
-import pytest
+import json
 from typing import List
 
-from rich.highlighter import NullHighlighter, ReprHighlighter
+import pytest
+
+from rich.highlighter import JSONHighlighter, NullHighlighter, ReprHighlighter
 from rich.text import Span, Text
 
 
@@ -40,6 +42,16 @@ def test_wrong_type():
             Span(4, 9, "repr.str"),
         ],
     ),
+    (
+        "",
+        [
+            Span(0, 1, "repr.tag_start"),
+            Span(1, 23, "repr.tag_name"),
+            Span(23, 25, "repr.tag_contents"),
+            Span(25, 26, "repr.tag_end"),
+            Span(24, 25, "repr.number"),
+        ],
+    ),
     ("( )", [Span(0, 1, "repr.brace"), Span(2, 3, "repr.brace")]),
     ("[ ]", [Span(0, 1, "repr.brace"), Span(2, 3, "repr.brace")]),
     ("{ }", [Span(0, 1, "repr.brace"), Span(2, 3, "repr.brace")]),
@@ -82,3 +94,53 @@ def test_highlight_regex(test: str, spans: List[Span]):
     highlighter.highlight(text)
     print(text.spans)
     assert text.spans == spans
+
+
+def test_highlight_json_with_indent():
+    json_string = json.dumps({"name": "apple", "count": 1}, indent=4)
+    text = Text(json_string)
+    highlighter = JSONHighlighter()
+    highlighter.highlight(text)
+    assert text.spans == [
+        Span(0, 1, "json.brace"),
+        Span(6, 12, "json.str"),
+        Span(14, 21, "json.str"),
+        Span(27, 34, "json.str"),
+        Span(36, 37, "json.number"),
+        Span(38, 39, "json.brace"),
+        Span(6, 12, "json.key"),
+        Span(27, 34, "json.key"),
+    ]
+
+
+def test_highlight_json_string_only():
+    json_string = '"abc"'
+    text = Text(json_string)
+    highlighter = JSONHighlighter()
+    highlighter.highlight(text)
+    assert text.spans == [Span(0, 5, "json.str")]
+
+
+def test_highlight_json_empty_string_only():
+    json_string = '""'
+    text = Text(json_string)
+    highlighter = JSONHighlighter()
+    highlighter.highlight(text)
+    assert text.spans == [Span(0, 2, "json.str")]
+
+
+def test_highlight_json_no_indent():
+    json_string = json.dumps({"name": "apple", "count": 1}, indent=None)
+    text = Text(json_string)
+    highlighter = JSONHighlighter()
+    highlighter.highlight(text)
+    assert text.spans == [
+        Span(0, 1, "json.brace"),
+        Span(1, 7, "json.str"),
+        Span(9, 16, "json.str"),
+        Span(18, 25, "json.str"),
+        Span(27, 28, "json.number"),
+        Span(28, 29, "json.brace"),
+        Span(1, 7, "json.key"),
+        Span(18, 25, "json.key"),
+    ]
diff --git a/tests/test_inspect.py b/tests/test_inspect.py
index 63c5f06244..b4c1d2a709 100644
--- a/tests/test_inspect.py
+++ b/tests/test_inspect.py
@@ -32,6 +32,11 @@
     reason="rendered differently on py3.10",
 )
 
+skip_pypy3 = pytest.mark.skipif(
+    hasattr(sys, "pypy_version_info"),
+    reason="rendered differently on pypy3",
+)
+
 
 def render(obj, methods=False, value=False, width=50) -> str:
     console = Console(file=io.StringIO(), width=width, legacy_windows=False)
@@ -81,6 +86,7 @@ def test_render():
     assert expected == result
 
 
+@skip_pypy3
 def test_inspect_text():
     expected = (
         "╭────────────────  ─────────────────╮\n"
@@ -98,6 +104,7 @@ def test_inspect_text():
 
 @skip_py36
 @skip_py37
+@skip_pypy3
 def test_inspect_empty_dict():
     expected = (
         "╭────────────────  ────────────────╮\n"
@@ -119,6 +126,7 @@ def test_inspect_empty_dict():
     assert render({}).startswith(expected)
 
 
+@skip_pypy3
 def test_inspect_builtin_function():
     expected = (
         "╭──────────  ───────────╮\n"
@@ -237,6 +245,7 @@ def test_inspect_integer_with_methods():
 
 @skip_py36
 @skip_py37
+@skip_pypy3
 def test_broken_call_attr():
     class NotCallable:
         __call__ = 5  # Passes callable() but isn't really callable
diff --git a/tests/test_layout.py b/tests/test_layout.py
index 61cc9b6b5d..46f496b0c7 100644
--- a/tests/test_layout.py
+++ b/tests/test_layout.py
@@ -1,4 +1,5 @@
 import sys
+
 import pytest
 
 from rich.console import Console
@@ -77,7 +78,7 @@ def test_tree():
         console.print(layout.tree, height=10)
     result = capture.get()
     print(repr(result))
-    expected = "⬍ Layout(name='root')                                       \n├── ⬍ Layout(size=2)                                        \n└── ⬌ Layout(name='bar')                                    \n    ├── ⬍ Layout()                                          \n    └── ⬍ Layout()                                          \n"
+    expected = "⬍ Layout(name='root')\n├── ⬍ Layout(size=2)\n└── ⬌ Layout(name='bar')\n    ├── ⬍ Layout()\n    └── ⬍ Layout()\n"
     print(result, "\n", expected)
     assert result == expected
 
diff --git a/tests/test_markup.py b/tests/test_markup.py
index 5a0acbbf09..106738bdb5 100644
--- a/tests/test_markup.py
+++ b/tests/test_markup.py
@@ -1,7 +1,7 @@
 import pytest
 
 from rich.console import Console
-from rich.markup import escape, MarkupError, _parse, render, Tag, RE_TAGS
+from rich.markup import RE_TAGS, MarkupError, Tag, _parse, escape, render
 from rich.text import Span
 
 
@@ -139,6 +139,11 @@ def test_markup_error():
         assert render("[foo]hello[/bar]")
 
 
+def test_markup_escape():
+    result = str(render("[dim white]\[url=[/]"))
+    assert result == "[url="
+
+
 def test_escape_escape():
     # Escaped escapes (i.e. double backslash)should be treated as literal
     result = render(r"\\[bold]FOO")
@@ -165,7 +170,6 @@ def test_escape_escape():
 
 
 def test_events():
-
     result = render("[@click]Hello[/@click] [@click='view.toggle', 'left']World[/]")
     assert str(result) == "Hello World"
 
diff --git a/tests/test_pretty.py b/tests/test_pretty.py
index 871f1397cc..d45b0c22db 100644
--- a/tests/test_pretty.py
+++ b/tests/test_pretty.py
@@ -1,15 +1,17 @@
+import collections
 import io
 import sys
 from array import array
-from collections import defaultdict, UserDict
-from typing import List
+from collections import UserDict, defaultdict
+from dataclasses import dataclass, field
+from typing import List, NamedTuple
 
 import attr
 import pytest
-from dataclasses import dataclass, field
 
 from rich.console import Console
-from rich.pretty import install, Pretty, pprint, pretty_repr, Node, _ipy_display_hook
+from rich.measure import Measurement
+from rich.pretty import Node, Pretty, _ipy_display_hook, install, pprint, pretty_repr
 from rich.text import Text
 
 skip_py36 = pytest.mark.skipif(
@@ -168,6 +170,74 @@ def test_pretty_dataclass():
     assert result == "ExampleDataclass(foo=1000, bar=..., baz=['foo', 'bar', 'baz'])"
 
 
+class StockKeepingUnit(NamedTuple):
+    name: str
+    description: str
+    price: float
+    category: str
+    reviews: List[str]
+
+
+def test_pretty_namedtuple():
+    console = Console(color_system=None)
+    console.begin_capture()
+
+    example_namedtuple = StockKeepingUnit(
+        "Sparkling British Spring Water",
+        "Carbonated spring water",
+        0.9,
+        "water",
+        ["its amazing!", "its terrible!"],
+    )
+
+    result = pretty_repr(example_namedtuple)
+
+    print(result)
+    assert (
+        result
+        == """StockKeepingUnit(
+    name='Sparkling British Spring Water',
+    description='Carbonated spring water',
+    price=0.9,
+    category='water',
+    reviews=['its amazing!', 'its terrible!']
+)"""
+    )
+
+
+def test_pretty_namedtuple_length_one_no_trailing_comma():
+    instance = collections.namedtuple("Thing", ["name"])(name="Bob")
+    assert pretty_repr(instance) == "Thing(name='Bob')"
+
+
+def test_pretty_namedtuple_empty():
+    instance = collections.namedtuple("Thing", [])()
+    assert pretty_repr(instance) == "Thing()"
+
+
+def test_pretty_namedtuple_custom_repr():
+    class Thing(NamedTuple):
+        def __repr__(self):
+            return "XX"
+
+    assert pretty_repr(Thing()) == "XX"
+
+
+def test_pretty_namedtuple_fields_invalid_type():
+    class LooksLikeANamedTupleButIsnt(tuple):
+        _fields = "blah"
+
+    instance = LooksLikeANamedTupleButIsnt()
+    result = pretty_repr(instance)
+    assert result == "()"  # Treated as tuple
+
+
+def test_pretty_namedtuple_max_depth():
+    instance = {"unit": StockKeepingUnit("a", "b", 1.0, "c", ["d", "e"])}
+    result = pretty_repr(instance, max_depth=1)
+    assert result == "{'unit': ...}"
+
+
 def test_small_width():
     test = ["Hello world! 12345"]
     result = pretty_repr(test, max_width=10)
@@ -464,3 +534,13 @@ def __getattr__(self, attr):
     foo = Foo()
     result = pretty_repr(foo)
     assert "Foo" in result
+
+
+def test_measure_pretty():
+    """Test measure respects expand_all"""
+    # https://github.com/Textualize/rich/issues/1998
+    console = Console()
+    pretty = Pretty(["alpha", "beta", "delta", "gamma"], expand_all=True)
+
+    measurement = console.measure(pretty)
+    assert measurement == Measurement(12, 12)
diff --git a/tests/test_progress.py b/tests/test_progress.py
index 2020f91ffb..db2e825d3a 100644
--- a/tests/test_progress.py
+++ b/tests/test_progress.py
@@ -2,6 +2,7 @@
 
 import io
 from time import sleep
+from types import SimpleNamespace
 
 import pytest
 
@@ -16,6 +17,7 @@
     TransferSpeedColumn,
     RenderableColumn,
     SpinnerColumn,
+    MofNCompleteColumn,
     Progress,
     Task,
     TextColumn,
@@ -88,6 +90,33 @@ class FakeTask(Task):
     assert str(text) == "0:01:00"
 
 
+@pytest.mark.parametrize(
+    "task_time, formatted",
+    [
+        (None, "--:--"),
+        (0, "00:00"),
+        (59, "00:59"),
+        (71, "01:11"),
+        (4210, "1:10:10"),
+    ],
+)
+def test_compact_time_remaining_column(task_time, formatted):
+    task = SimpleNamespace(finished=False, time_remaining=task_time)
+    column = TimeRemainingColumn(compact=True)
+
+    assert str(column.render(task)) == formatted
+
+
+def test_time_remaining_column_elapsed_when_finished():
+    task_time = 71
+    formatted = "0:01:11"
+
+    task = SimpleNamespace(finished=True, finished_time=task_time)
+    column = TimeRemainingColumn(elapsed_when_finished=True)
+
+    assert str(column.render(task)) == formatted
+
+
 def test_renderable_column():
     column = RenderableColumn("foo")
     task = Task(1, "test", 100, 20, _get_time=lambda: 1.0)
@@ -122,7 +151,7 @@ def test_download_progress_uses_decimal_units() -> None:
     column = DownloadColumn()
     test_task = Task(1, "test", 1000, 500, _get_time=lambda: 1.0)
     rendered_progress = str(column.render(test_task))
-    expected = "0.5/1.0 KB"
+    expected = "0.5/1.0 kB"
     assert rendered_progress == expected
 
 
@@ -311,6 +340,8 @@ def test_columns() -> None:
         TotalFileSizeColumn(),
         DownloadColumn(),
         TransferSpeedColumn(),
+        MofNCompleteColumn(),
+        MofNCompleteColumn(separator=" of "),
         transient=True,
         console=console,
         auto_refresh=False,
@@ -330,10 +361,37 @@ def test_columns() -> None:
 
     result = replace_link_ids(console.file.getvalue())
     print(repr(result))
-    expected = "\x1b[?25ltest foo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:07\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m\ntest bar \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:16\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m\r\x1b[2K\x1b[1A\x1b[2Kfoo\ntest foo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:07\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m\ntest bar \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:16\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m\r\x1b[2K\x1b[1A\x1b[2K\x1b[2;36m[TIME]\x1b[0m\x1b[2;36m \x1b[0mhello                                                                    \ntest foo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:07\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m\ntest bar \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:16\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m\r\x1b[2K\x1b[1A\x1b[2Kworld\ntest foo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:07\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m\ntest bar \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:16\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m\r\x1b[2K\x1b[1A\x1b[2Ktest foo \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[33m0:00:30\x1b[0m \x1b[32m12 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m12/10 bytes\x1b[0m \x1b[31m1 byte/s \x1b[0m\ntest bar \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[33m0:00:25\x1b[0m \x1b[32m16 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m16/7 bytes \x1b[0m \x1b[31m2 bytes/s\x1b[0m\r\x1b[2K\x1b[1A\x1b[2Ktest foo \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[33m0:00:30\x1b[0m \x1b[32m12 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m12/10 bytes\x1b[0m \x1b[31m1 byte/s \x1b[0m\ntest bar \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[33m0:00:25\x1b[0m \x1b[32m16 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m16/7 bytes \x1b[0m \x1b[31m2 bytes/s\x1b[0m\n\x1b[?25h\r\x1b[1A\x1b[2K\x1b[1A\x1b[2K"
+    expected = "\x1b[?25ltest foo \x1b[38;5;237m━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:07\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m \x1b[32m 0/10\x1b[0m \x1b[32m 0 of 10\x1b[0m\ntest bar \x1b[38;5;237m━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:18\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m \x1b[32m0/7  \x1b[0m \x1b[32m0 of 7  \x1b[0m\r\x1b[2K\x1b[1A\x1b[2Kfoo\ntest foo \x1b[38;5;237m━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:07\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m \x1b[32m 0/10\x1b[0m \x1b[32m 0 of 10\x1b[0m\ntest bar \x1b[38;5;237m━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:18\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m \x1b[32m0/7  \x1b[0m \x1b[32m0 of 7  \x1b[0m\r\x1b[2K\x1b[1A\x1b[2K\x1b[2;36m[TIME]\x1b[0m\x1b[2;36m \x1b[0mhello                                                                    \ntest foo \x1b[38;5;237m━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:07\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m \x1b[32m 0/10\x1b[0m \x1b[32m 0 of 10\x1b[0m\ntest bar \x1b[38;5;237m━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:18\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m \x1b[32m0/7  \x1b[0m \x1b[32m0 of 7  \x1b[0m\r\x1b[2K\x1b[1A\x1b[2Kworld\ntest foo \x1b[38;5;237m━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:07\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m \x1b[32m 0/10\x1b[0m \x1b[32m 0 of 10\x1b[0m\ntest bar \x1b[38;5;237m━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:18\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m \x1b[32m0/7  \x1b[0m \x1b[32m0 of 7  \x1b[0m\r\x1b[2K\x1b[1A\x1b[2Ktest foo \x1b[38;2;114;156;31m━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[33m0:00:34\x1b[0m \x1b[32m12     \x1b[0m \x1b[32m10     \x1b[0m \x1b[32m12/10   \x1b[0m \x1b[31m1      \x1b[0m \x1b[32m12/10\x1b[0m \x1b[32m12 of 10\x1b[0m\n                                 \x1b[32mbytes  \x1b[0m \x1b[32mbytes  \x1b[0m \x1b[32mbytes   \x1b[0m \x1b[31mbyte/s \x1b[0m               \ntest bar \x1b[38;2;114;156;31m━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[33m0:00:29\x1b[0m \x1b[32m16     \x1b[0m \x1b[32m7 bytes\x1b[0m \x1b[32m16/7    \x1b[0m \x1b[31m2      \x1b[0m \x1b[32m16/7 \x1b[0m \x1b[32m16 of 7 \x1b[0m\n                                 \x1b[32mbytes  \x1b[0m         \x1b[32mbytes   \x1b[0m \x1b[31mbytes/s\x1b[0m               \r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2Ktest foo \x1b[38;2;114;156;31m━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[33m0:00:34\x1b[0m \x1b[32m12     \x1b[0m \x1b[32m10     \x1b[0m \x1b[32m12/10   \x1b[0m \x1b[31m1      \x1b[0m \x1b[32m12/10\x1b[0m \x1b[32m12 of 10\x1b[0m\n                                 \x1b[32mbytes  \x1b[0m \x1b[32mbytes  \x1b[0m \x1b[32mbytes   \x1b[0m \x1b[31mbyte/s \x1b[0m               \ntest bar \x1b[38;2;114;156;31m━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[33m0:00:29\x1b[0m \x1b[32m16     \x1b[0m \x1b[32m7 bytes\x1b[0m \x1b[32m16/7    \x1b[0m \x1b[31m2      \x1b[0m \x1b[32m16/7 \x1b[0m \x1b[32m16 of 7 \x1b[0m\n                                 \x1b[32mbytes  \x1b[0m         \x1b[32mbytes   \x1b[0m \x1b[31mbytes/s\x1b[0m               \n\x1b[?25h\r\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K"
+
     assert result == expected
 
 
+def test_using_default_columns() -> None:
+    # can only check types, as the instances do not '==' each other
+    expected_default_types = [
+        TextColumn,
+        BarColumn,
+        TextColumn,
+        TimeRemainingColumn,
+    ]
+
+    progress = Progress()
+    assert [type(c) for c in progress.columns] == expected_default_types
+
+    progress = Progress(
+        SpinnerColumn(),
+        *Progress.get_default_columns(),
+        "Elapsed:",
+        TimeElapsedColumn(),
+    )
+    assert [type(c) for c in progress.columns] == [
+        SpinnerColumn,
+        *expected_default_types,
+        str,
+        TimeElapsedColumn,
+    ]
+
+
 def test_task_create() -> None:
     task = Task(TaskID(1), "foo", 100, 0, _get_time=lambda: 1)
     assert task.elapsed is None
diff --git a/tests/test_syntax.py b/tests/test_syntax.py
index 46d0126e14..e5d904f363 100644
--- a/tests/test_syntax.py
+++ b/tests/test_syntax.py
@@ -277,7 +277,7 @@ def test_from_path_lexer_override():
     try:
         os.write(fh, b"import this\n")
         syntax = Syntax.from_path(path, lexer="rust")
-        assert syntax.lexer.name is "Rust"
+        assert syntax.lexer.name == "Rust"
         assert syntax.code == "import this\n"
     finally:
         os.remove(path)
diff --git a/tests/test_tabulate.py b/tests/test_tabulate.py
deleted file mode 100644
index 37e86bfe2b..0000000000
--- a/tests/test_tabulate.py
+++ /dev/null
@@ -1,34 +0,0 @@
-import itertools
-from rich.style import Style
-from rich.table import _Cell
-from rich.tabulate import tabulate_mapping
-
-
-def test_tabulate_mapping():
-    # TODO: tabulate_mapping may not be needed shortly
-    table = tabulate_mapping({"foo": "1", "bar": "2"})
-    assert len(table.columns) == 2
-    assert len(table.columns[0]._cells) == 2
-    assert len(table.columns[1]._cells) == 2
-
-    # add tests for title and caption justification
-    test_title = "Foo v. Bar"
-    test_caption = "approximate results"
-    for title_justify, caption_justify in itertools.product(
-        [None, "left", "center", "right"], repeat=2
-    ):
-        table = tabulate_mapping(
-            {"foo": "1", "bar": "2"},
-            title=test_title,
-            caption=test_caption,
-            title_justify=title_justify,
-            caption_justify=caption_justify,
-        )
-        expected_title_justify = (
-            title_justify if title_justify is not None else "center"
-        )
-        expected_caption_justify = (
-            caption_justify if caption_justify is not None else "center"
-        )
-        assert expected_title_justify == table.title_justify
-        assert expected_caption_justify == table.caption_justify
diff --git a/tests/test_traceback.py b/tests/test_traceback.py
index e3342ca13c..31658c5a43 100644
--- a/tests/test_traceback.py
+++ b/tests/test_traceback.py
@@ -110,8 +110,8 @@ def test_syntax_error():
     console = Console(width=100, file=io.StringIO())
     try:
         # raises SyntaxError: unexpected EOF while parsing
-        compile("(2+2")
-    except Exception:
+        eval("(2+2")
+    except SyntaxError:
         console.print_exception()
     exception_text = console.file.getvalue()
     assert "SyntaxError" in exception_text
diff --git a/tests/test_tree.py b/tests/test_tree.py
index f5510aa536..c8465a97a9 100644
--- a/tests/test_tree.py
+++ b/tests/test_tree.py
@@ -12,7 +12,7 @@ def test_render_single_node():
     console = Console(color_system=None, width=20)
     console.begin_capture()
     console.print(tree)
-    assert console.end_capture() == "foo                 \n"
+    assert console.end_capture() == "foo\n"
 
 
 def test_render_single_branch():
@@ -23,7 +23,7 @@ def test_render_single_branch():
     console.print(tree)
     result = console.end_capture()
     print(repr(result))
-    expected = "foo                 \n└── bar             \n"
+    expected = "foo\n└── bar\n"
     assert result == expected
 
 
@@ -36,7 +36,7 @@ def test_render_double_branch():
     console.print(tree)
     result = console.end_capture()
     print(repr(result))
-    expected = "foo                 \n├── bar             \n└── baz             \n"
+    expected = "foo\n├── bar\n└── baz\n"
     assert result == expected
 
 
@@ -54,7 +54,7 @@ def encoding(self):
     console.begin_capture()
     console.print(tree)
     result = console.end_capture()
-    expected = "foo                 \n+-- bar             \n`-- baz             \n"
+    expected = "foo\n+-- bar\n`-- baz\n"
     assert result == expected
 
 
@@ -74,7 +74,7 @@ def test_render_tree_non_win32():
     console.print(tree)
     result = console.end_capture()
     print(repr(result))
-    expected = "foo                 \n├── \x1b[3mbar\x1b[0m\x1b[3m             \x1b[0m\n\x1b[44m├── \x1b[0m\x1b[44mbaz\x1b[0m\x1b[44m             \x1b[0m\n\x1b[44m│   \x1b[0m\x1b[31;44m┣━━ \x1b[0m\x1b[44m1\x1b[0m\x1b[44m           \x1b[0m\n\x1b[44m│   \x1b[0m\x1b[31;44m┗━━ \x1b[0m\x1b[44m2\x1b[0m\x1b[44m           \x1b[0m\n└── egg             \n"
+    expected = "foo\n├── \x1b[3mbar\x1b[0m\n\x1b[44m├── \x1b[0m\x1b[44mbaz\x1b[0m\n\x1b[44m│   \x1b[0m\x1b[31;44m┣━━ \x1b[0m\x1b[44m1\x1b[0m\n\x1b[44m│   \x1b[0m\x1b[31;44m┗━━ \x1b[0m\x1b[44m2\x1b[0m\n└── egg\n"
     assert result == expected
 
 
@@ -87,12 +87,14 @@ def test_render_tree_win32():
     baz_tree.add("2")
     tree.add("egg")
 
-    console = Console(width=20, force_terminal=True, color_system="standard")
+    console = Console(
+        width=20, force_terminal=True, color_system="standard", legacy_windows=True
+    )
     console.begin_capture()
     console.print(tree)
     result = console.end_capture()
     print(repr(result))
-    expected = "foo                 \n├── \x1b[3mbar\x1b[0m\x1b[3m             \x1b[0m\n\x1b[44m├── \x1b[0m\x1b[44mbaz\x1b[0m\x1b[44m             \x1b[0m\n\x1b[44m│   \x1b[0m\x1b[31;44m├── \x1b[0m\x1b[44m1\x1b[0m\x1b[44m           \x1b[0m\n\x1b[44m│   \x1b[0m\x1b[31;44m└── \x1b[0m\x1b[44m2\x1b[0m\x1b[44m           \x1b[0m\n└── egg             \n"
+    expected = "foo\n├── \x1b[3mbar\x1b[0m\n\x1b[44m├── \x1b[0m\x1b[44mbaz\x1b[0m\n\x1b[44m│   \x1b[0m\x1b[31;44m├── \x1b[0m\x1b[44m1\x1b[0m\n\x1b[44m│   \x1b[0m\x1b[31;44m└── \x1b[0m\x1b[44m2\x1b[0m\n└── egg\n"
     assert result == expected
 
 
@@ -112,7 +114,7 @@ def test_render_tree_hide_root_non_win32():
     console.print(tree)
     result = console.end_capture()
     print(repr(result))
-    expected = "\x1b[3mbar\x1b[0m\x1b[3m                 \x1b[0m\n\x1b[44mbaz\x1b[0m\x1b[44m                 \x1b[0m\n\x1b[31;44m┣━━ \x1b[0m\x1b[44m1\x1b[0m\x1b[44m               \x1b[0m\n\x1b[31;44m┗━━ \x1b[0m\x1b[44m2\x1b[0m\x1b[44m               \x1b[0m\negg                 \n"
+    expected = "\x1b[3mbar\x1b[0m\n\x1b[44mbaz\x1b[0m\n\x1b[31;44m┣━━ \x1b[0m\x1b[44m1\x1b[0m\n\x1b[31;44m┗━━ \x1b[0m\x1b[44m2\x1b[0m\negg\n"
     assert result == expected
 
 
@@ -130,7 +132,7 @@ def test_render_tree_hide_root_win32():
     console.print(tree)
     result = console.end_capture()
     print(repr(result))
-    expected = "\x1b[3mbar\x1b[0m\x1b[3m                 \x1b[0m\n\x1b[44mbaz\x1b[0m\x1b[44m                 \x1b[0m\n\x1b[31;44m├── \x1b[0m\x1b[44m1\x1b[0m\x1b[44m               \x1b[0m\n\x1b[31;44m└── \x1b[0m\x1b[44m2\x1b[0m\x1b[44m               \x1b[0m\negg                 \n"
+    expected = "\x1b[3mbar\x1b[0m\n\x1b[44mbaz\x1b[0m\n\x1b[31;44m├── \x1b[0m\x1b[44m1\x1b[0m\n\x1b[31;44m└── \x1b[0m\x1b[44m2\x1b[0m\negg\n"
     assert result == expected
 
 
diff --git a/tests/test_win32_console.py b/tests/test_win32_console.py
new file mode 100644
index 0000000000..4523600d63
--- /dev/null
+++ b/tests/test_win32_console.py
@@ -0,0 +1,438 @@
+import dataclasses
+import sys
+from unittest import mock
+from unittest.mock import patch
+
+import pytest
+
+from rich.style import Style
+
+if sys.platform == "win32":
+    from rich import _win32_console
+    from rich._win32_console import COORD, LegacyWindowsTerm, WindowsCoordinates
+
+    CURSOR_X = 1
+    CURSOR_Y = 2
+    CURSOR_POSITION = WindowsCoordinates(row=CURSOR_Y, col=CURSOR_X)
+    SCREEN_WIDTH = 20
+    SCREEN_HEIGHT = 30
+    DEFAULT_STYLE_ATTRIBUTE = 16
+
+    @dataclasses.dataclass
+    class StubScreenBufferInfo:
+        dwCursorPosition: COORD = COORD(CURSOR_X, CURSOR_Y)
+        dwSize: COORD = COORD(SCREEN_WIDTH, SCREEN_HEIGHT)
+        wAttributes: int = DEFAULT_STYLE_ATTRIBUTE
+
+    pytestmark = pytest.mark.skipif(sys.platform != "win32", reason="windows only")
+
+    def test_windows_coordinates_to_ctype():
+        coord = WindowsCoordinates.from_param(WindowsCoordinates(row=1, col=2))
+        assert coord.X == 2
+        assert coord.Y == 1
+
+    @pytest.fixture
+    def win32_handle():
+        handle = mock.sentinel
+        with mock.patch.object(_win32_console, "GetStdHandle", return_value=handle):
+            yield handle
+
+    @patch.object(
+        _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo
+    )
+    def test_cursor_position(_):
+        term = LegacyWindowsTerm()
+        assert term.cursor_position == WindowsCoordinates(row=CURSOR_Y, col=CURSOR_X)
+
+    @patch.object(
+        _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo
+    )
+    def test_screen_size(_):
+        term = LegacyWindowsTerm()
+        assert term.screen_size == WindowsCoordinates(
+            row=SCREEN_HEIGHT, col=SCREEN_WIDTH
+        )
+
+    @patch.object(_win32_console, "WriteConsole", return_value=True)
+    @patch.object(
+        _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo
+    )
+    def test_write_text(_, WriteConsole, win32_handle):
+        text = "Hello, world!"
+        term = LegacyWindowsTerm()
+
+        term.write_text(text)
+
+        WriteConsole.assert_called_once_with(win32_handle, text)
+
+    @patch.object(_win32_console, "WriteConsole", return_value=True)
+    @patch.object(_win32_console, "SetConsoleTextAttribute")
+    @patch.object(
+        _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo
+    )
+    def test_write_styled(_, SetConsoleTextAttribute, WriteConsole, win32_handle):
+        style = Style.parse("black on red")
+        text = "Hello, world!"
+        term = LegacyWindowsTerm()
+
+        term.write_styled(text, style)
+
+        # Check that we've called the Console API to write the text
+        call_args = WriteConsole.call_args_list
+        assert len(call_args) == 1
+        args, _ = call_args[0]
+        assert args == (win32_handle, text)
+
+        # Ensure we set the text attributes and then reset them after writing styled text
+        call_args = SetConsoleTextAttribute.call_args_list
+        assert len(call_args) == 2
+        first_args, first_kwargs = call_args[0]
+        second_args, second_kwargs = call_args[1]
+
+        assert first_args == (win32_handle,)
+        assert first_kwargs["attributes"].value == 64
+        assert second_args == (win32_handle,)
+        assert second_kwargs["attributes"] == DEFAULT_STYLE_ATTRIBUTE
+
+    @patch.object(_win32_console, "WriteConsole", return_value=True)
+    @patch.object(_win32_console, "SetConsoleTextAttribute")
+    @patch.object(
+        _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo
+    )
+    def test_write_styled_bold(_, SetConsoleTextAttribute, __, win32_handle):
+        style = Style.parse("bold black on red")
+        text = "Hello, world!"
+        term = LegacyWindowsTerm()
+
+        term.write_styled(text, style)
+
+        call_args = SetConsoleTextAttribute.call_args_list
+        first_args, first_kwargs = call_args[0]
+
+        expected_attr = 64 + 8  # 64 for red bg, +8 for bright black
+        assert first_args == (win32_handle,)
+        assert first_kwargs["attributes"].value == expected_attr
+
+    @patch.object(_win32_console, "WriteConsole", return_value=True)
+    @patch.object(_win32_console, "SetConsoleTextAttribute")
+    @patch.object(
+        _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo
+    )
+    def test_write_styled_reverse(_, SetConsoleTextAttribute, __, win32_handle):
+        style = Style.parse("reverse red on blue")
+        text = "Hello, world!"
+        term = LegacyWindowsTerm()
+
+        term.write_styled(text, style)
+
+        call_args = SetConsoleTextAttribute.call_args_list
+        first_args, first_kwargs = call_args[0]
+
+        expected_attr = 64 + 1  # 64 for red bg (after reverse), +1 for blue fg
+        assert first_args == (win32_handle,)
+        assert first_kwargs["attributes"].value == expected_attr
+
+    @patch.object(_win32_console, "WriteConsole", return_value=True)
+    @patch.object(_win32_console, "SetConsoleTextAttribute")
+    @patch.object(
+        _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo
+    )
+    def test_write_styled_reverse(_, SetConsoleTextAttribute, __, win32_handle):
+        style = Style.parse("dim bright_red on blue")
+        text = "Hello, world!"
+        term = LegacyWindowsTerm()
+
+        term.write_styled(text, style)
+
+        call_args = SetConsoleTextAttribute.call_args_list
+        first_args, first_kwargs = call_args[0]
+
+        expected_attr = 4 + 16  # 4 for red text (after dim), +16 for blue bg
+        assert first_args == (win32_handle,)
+        assert first_kwargs["attributes"].value == expected_attr
+
+    @patch.object(_win32_console, "WriteConsole", return_value=True)
+    @patch.object(_win32_console, "SetConsoleTextAttribute")
+    @patch.object(
+        _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo
+    )
+    def test_write_styled_no_foreground_color(
+        _, SetConsoleTextAttribute, __, win32_handle
+    ):
+        style = Style.parse("on blue")
+        text = "Hello, world!"
+        term = LegacyWindowsTerm()
+
+        term.write_styled(text, style)
+
+        call_args = SetConsoleTextAttribute.call_args_list
+        first_args, first_kwargs = call_args[0]
+
+        expected_attr = 16 | term._default_fore  # 16 for blue bg, plus default fg color
+        assert first_args == (win32_handle,)
+        assert first_kwargs["attributes"].value == expected_attr
+
+    @patch.object(_win32_console, "WriteConsole", return_value=True)
+    @patch.object(_win32_console, "SetConsoleTextAttribute")
+    @patch.object(
+        _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo
+    )
+    def test_write_styled_no_background_color(
+        _, SetConsoleTextAttribute, __, win32_handle
+    ):
+        style = Style.parse("blue")
+        text = "Hello, world!"
+        term = LegacyWindowsTerm()
+
+        term.write_styled(text, style)
+
+        call_args = SetConsoleTextAttribute.call_args_list
+        first_args, first_kwargs = call_args[0]
+
+        expected_attr = (
+            16 | term._default_back
+        )  # 16 for blue foreground, plus default bg color
+        assert first_args == (win32_handle,)
+        assert first_kwargs["attributes"].value == expected_attr
+
+    @patch.object(_win32_console, "FillConsoleOutputCharacter", return_value=None)
+    @patch.object(_win32_console, "FillConsoleOutputAttribute", return_value=None)
+    @patch.object(
+        _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo
+    )
+    def test_erase_line(
+        _, FillConsoleOutputAttribute, FillConsoleOutputCharacter, win32_handle
+    ):
+        term = LegacyWindowsTerm()
+        term.erase_line()
+        start = WindowsCoordinates(row=CURSOR_Y, col=0)
+        FillConsoleOutputCharacter.assert_called_once_with(
+            win32_handle, " ", length=SCREEN_WIDTH, start=start
+        )
+        FillConsoleOutputAttribute.assert_called_once_with(
+            win32_handle, DEFAULT_STYLE_ATTRIBUTE, length=SCREEN_WIDTH, start=start
+        )
+
+    @patch.object(_win32_console, "FillConsoleOutputCharacter", return_value=None)
+    @patch.object(_win32_console, "FillConsoleOutputAttribute", return_value=None)
+    @patch.object(
+        _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo
+    )
+    def test_erase_end_of_line(
+        _, FillConsoleOutputAttribute, FillConsoleOutputCharacter, win32_handle
+    ):
+        term = LegacyWindowsTerm()
+        term.erase_end_of_line()
+
+        FillConsoleOutputCharacter.assert_called_once_with(
+            win32_handle, " ", length=SCREEN_WIDTH - CURSOR_X, start=CURSOR_POSITION
+        )
+        FillConsoleOutputAttribute.assert_called_once_with(
+            win32_handle,
+            DEFAULT_STYLE_ATTRIBUTE,
+            length=SCREEN_WIDTH - CURSOR_X,
+            start=CURSOR_POSITION,
+        )
+
+    @patch.object(_win32_console, "FillConsoleOutputCharacter", return_value=None)
+    @patch.object(_win32_console, "FillConsoleOutputAttribute", return_value=None)
+    @patch.object(
+        _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo
+    )
+    def test_erase_start_of_line(
+        _, FillConsoleOutputAttribute, FillConsoleOutputCharacter, win32_handle
+    ):
+        term = LegacyWindowsTerm()
+        term.erase_start_of_line()
+
+        start = WindowsCoordinates(CURSOR_Y, 0)
+
+        FillConsoleOutputCharacter.assert_called_once_with(
+            win32_handle, " ", length=CURSOR_X, start=start
+        )
+        FillConsoleOutputAttribute.assert_called_once_with(
+            win32_handle, DEFAULT_STYLE_ATTRIBUTE, length=CURSOR_X, start=start
+        )
+
+    @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None)
+    @patch.object(
+        _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo
+    )
+    def test_move_cursor_to(_, SetConsoleCursorPosition, win32_handle):
+        coords = WindowsCoordinates(row=4, col=5)
+        term = LegacyWindowsTerm()
+
+        term.move_cursor_to(coords)
+
+        SetConsoleCursorPosition.assert_called_once_with(win32_handle, coords=coords)
+
+    @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None)
+    @patch.object(
+        _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo
+    )
+    def test_move_cursor_to_out_of_bounds_row(
+        _, SetConsoleCursorPosition, win32_handle
+    ):
+        coords = WindowsCoordinates(row=-1, col=4)
+        term = LegacyWindowsTerm()
+
+        term.move_cursor_to(coords)
+
+        assert not SetConsoleCursorPosition.called
+
+    @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None)
+    @patch.object(
+        _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo
+    )
+    def test_move_cursor_to_out_of_bounds_col(
+        _, SetConsoleCursorPosition, win32_handle
+    ):
+        coords = WindowsCoordinates(row=10, col=-4)
+        term = LegacyWindowsTerm()
+
+        term.move_cursor_to(coords)
+
+        assert not SetConsoleCursorPosition.called
+
+    @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None)
+    @patch.object(
+        _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo
+    )
+    def test_move_cursor_up(_, SetConsoleCursorPosition, win32_handle):
+        term = LegacyWindowsTerm()
+
+        term.move_cursor_up()
+
+        SetConsoleCursorPosition.assert_called_once_with(
+            win32_handle, coords=WindowsCoordinates(row=CURSOR_Y - 1, col=CURSOR_X)
+        )
+
+    @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None)
+    @patch.object(
+        _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo
+    )
+    def test_move_cursor_down(_, SetConsoleCursorPosition, win32_handle):
+        term = LegacyWindowsTerm()
+
+        term.move_cursor_down()
+
+        SetConsoleCursorPosition.assert_called_once_with(
+            win32_handle, coords=WindowsCoordinates(row=CURSOR_Y + 1, col=CURSOR_X)
+        )
+
+    @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None)
+    @patch.object(
+        _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo
+    )
+    def test_move_cursor_forward(_, SetConsoleCursorPosition, win32_handle):
+        term = LegacyWindowsTerm()
+
+        term.move_cursor_forward()
+
+        SetConsoleCursorPosition.assert_called_once_with(
+            win32_handle, coords=WindowsCoordinates(row=CURSOR_Y, col=CURSOR_X + 1)
+        )
+
+    @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None)
+    def test_move_cursor_forward_newline_wrap(SetConsoleCursorPosition, win32_handle):
+        cursor_at_end_of_line = StubScreenBufferInfo(
+            dwCursorPosition=COORD(SCREEN_WIDTH - 1, CURSOR_Y)
+        )
+        with patch.object(
+            _win32_console,
+            "GetConsoleScreenBufferInfo",
+            return_value=cursor_at_end_of_line,
+        ):
+            term = LegacyWindowsTerm()
+            term.move_cursor_forward()
+
+        SetConsoleCursorPosition.assert_called_once_with(
+            win32_handle, coords=WindowsCoordinates(row=CURSOR_Y + 1, col=0)
+        )
+
+    @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None)
+    @patch.object(
+        _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo
+    )
+    def test_move_cursor_to_column(_, SetConsoleCursorPosition, win32_handle):
+        term = LegacyWindowsTerm()
+        term.move_cursor_to_column(5)
+        SetConsoleCursorPosition.assert_called_once_with(
+            win32_handle, coords=WindowsCoordinates(CURSOR_Y, 5)
+        )
+
+    @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None)
+    @patch.object(
+        _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo
+    )
+    def test_move_cursor_backward(_, SetConsoleCursorPosition, win32_handle):
+        term = LegacyWindowsTerm()
+        term.move_cursor_backward()
+        SetConsoleCursorPosition.assert_called_once_with(
+            win32_handle, coords=WindowsCoordinates(row=CURSOR_Y, col=CURSOR_X - 1)
+        )
+
+    @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None)
+    def test_move_cursor_backward_prev_line_wrap(
+        SetConsoleCursorPosition, win32_handle
+    ):
+        cursor_at_start_of_line = StubScreenBufferInfo(
+            dwCursorPosition=COORD(0, CURSOR_Y)
+        )
+        with patch.object(
+            _win32_console,
+            "GetConsoleScreenBufferInfo",
+            return_value=cursor_at_start_of_line,
+        ):
+            term = LegacyWindowsTerm()
+            term.move_cursor_backward()
+        SetConsoleCursorPosition.assert_called_once_with(
+            win32_handle,
+            coords=WindowsCoordinates(row=CURSOR_Y - 1, col=SCREEN_WIDTH - 1),
+        )
+
+    @patch.object(_win32_console, "SetConsoleCursorInfo", return_value=None)
+    @patch.object(
+        _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo
+    )
+    def test_hide_cursor(_, SetConsoleCursorInfo, win32_handle):
+        term = LegacyWindowsTerm()
+        term.hide_cursor()
+
+        call_args = SetConsoleCursorInfo.call_args_list
+
+        assert len(call_args) == 1
+
+        args, kwargs = call_args[0]
+        assert kwargs["cursor_info"].bVisible == 0
+        assert kwargs["cursor_info"].dwSize == 100
+
+    @patch.object(_win32_console, "SetConsoleCursorInfo", return_value=None)
+    @patch.object(
+        _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo
+    )
+    def test_show_cursor(_, SetConsoleCursorInfo, win32_handle):
+        term = LegacyWindowsTerm()
+        term.show_cursor()
+
+        call_args = SetConsoleCursorInfo.call_args_list
+
+        assert len(call_args) == 1
+
+        args, kwargs = call_args[0]
+        assert kwargs["cursor_info"].bVisible == 1
+        assert kwargs["cursor_info"].dwSize == 100
+
+    @patch.object(_win32_console, "SetConsoleTitle", return_value=None)
+    def test_set_title(SetConsoleTitle):
+        term = LegacyWindowsTerm()
+        term.set_title("title")
+
+        SetConsoleTitle.assert_called_once_with("title")
+
+    @patch.object(_win32_console, "SetConsoleTitle", return_value=None)
+    def test_set_title_too_long(_):
+        term = LegacyWindowsTerm()
+
+        with pytest.raises(AssertionError):
+            term.set_title("a" * 255)
diff --git a/tests/test_windows_renderer.py b/tests/test_windows_renderer.py
new file mode 100644
index 0000000000..ff2b273a70
--- /dev/null
+++ b/tests/test_windows_renderer.py
@@ -0,0 +1,133 @@
+import sys
+from unittest.mock import call, create_autospec
+
+import pytest
+
+try:
+    from rich._win32_console import LegacyWindowsTerm, WindowsCoordinates
+    from rich._windows_renderer import legacy_windows_render
+except:
+    # These modules can only be imported on Windows
+    pass
+from rich.segment import ControlType, Segment
+from rich.style import Style
+
+pytestmark = pytest.mark.skipif(sys.platform != "win32", reason="windows only")
+
+
+@pytest.fixture
+def legacy_term_mock():
+    return create_autospec(LegacyWindowsTerm)
+
+
+def test_text_only(legacy_term_mock):
+    text = "Hello, world!"
+    buffer = [Segment(text)]
+    legacy_windows_render(buffer, legacy_term_mock)
+
+    legacy_term_mock.write_text.assert_called_once_with(text)
+
+
+def test_text_multiple_segments(legacy_term_mock):
+    buffer = [Segment("Hello, "), Segment("world!")]
+    legacy_windows_render(buffer, legacy_term_mock)
+
+    assert legacy_term_mock.write_text.call_args_list == [
+        call("Hello, "),
+        call("world!"),
+    ]
+
+
+def test_text_with_style(legacy_term_mock):
+    text = "Hello, world!"
+    style = Style.parse("black on red")
+    buffer = [Segment(text, style)]
+
+    legacy_windows_render(buffer, legacy_term_mock)
+
+    legacy_term_mock.write_styled.assert_called_once_with(text, style)
+
+
+def test_control_cursor_move_to(legacy_term_mock):
+    buffer = [Segment("", None, [(ControlType.CURSOR_MOVE_TO, 20, 30)])]
+
+    legacy_windows_render(buffer, legacy_term_mock)
+
+    legacy_term_mock.move_cursor_to.assert_called_once_with(
+        WindowsCoordinates(row=29, col=19)
+    )
+
+
+def test_control_carriage_return(legacy_term_mock):
+    buffer = [Segment("", None, [(ControlType.CARRIAGE_RETURN,)])]
+
+    legacy_windows_render(buffer, legacy_term_mock)
+
+    legacy_term_mock.write_text.assert_called_once_with("\r")
+
+
+def test_control_home(legacy_term_mock):
+    buffer = [Segment("", None, [(ControlType.HOME,)])]
+
+    legacy_windows_render(buffer, legacy_term_mock)
+
+    legacy_term_mock.move_cursor_to.assert_called_once_with(WindowsCoordinates(0, 0))
+
+
+@pytest.mark.parametrize(
+    "control_type, method_name",
+    [
+        (ControlType.CURSOR_UP, "move_cursor_up"),
+        (ControlType.CURSOR_DOWN, "move_cursor_down"),
+        (ControlType.CURSOR_FORWARD, "move_cursor_forward"),
+        (ControlType.CURSOR_BACKWARD, "move_cursor_backward"),
+    ],
+)
+def test_control_cursor_single_cell_movement(
+    legacy_term_mock, control_type, method_name
+):
+    buffer = [Segment("", None, [(control_type,)])]
+
+    legacy_windows_render(buffer, legacy_term_mock)
+
+    getattr(legacy_term_mock, method_name).assert_called_once_with()
+
+
+@pytest.mark.parametrize(
+    "erase_mode, method_name",
+    [
+        (0, "erase_end_of_line"),
+        (1, "erase_start_of_line"),
+        (2, "erase_line"),
+    ],
+)
+def test_control_erase_line(legacy_term_mock, erase_mode, method_name):
+    buffer = [Segment("", None, [(ControlType.ERASE_IN_LINE, erase_mode)])]
+
+    legacy_windows_render(buffer, legacy_term_mock)
+
+    getattr(legacy_term_mock, method_name).assert_called_once_with()
+
+
+def test_control_show_cursor(legacy_term_mock):
+    buffer = [Segment("", None, [(ControlType.SHOW_CURSOR,)])]
+
+    legacy_windows_render(buffer, legacy_term_mock)
+
+    legacy_term_mock.show_cursor.assert_called_once_with()
+
+
+def test_control_hide_cursor(legacy_term_mock):
+    buffer = [Segment("", None, [(ControlType.HIDE_CURSOR,)])]
+
+    legacy_windows_render(buffer, legacy_term_mock)
+
+    legacy_term_mock.hide_cursor.assert_called_once_with()
+
+
+def test_control_cursor_move_to_column(legacy_term_mock):
+    buffer = [Segment("", None, [(ControlType.CURSOR_MOVE_TO_COLUMN, 3)])]
+
+    legacy_windows_render(buffer, legacy_term_mock)
+
+    legacy_term_mock.move_cursor_to_column.assert_called_once_with(2)